diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5a3f06c0068b199f00d8bc395b1e9a5e0d03e0ba..12e9b2e7837da3cae36be30e5f54ce343f75f5f3 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -20,7 +20,7 @@
 #
 
 services:
-  - mariadb:10.2
+  - mariadb:10.4
 variables:
   DEPLOY_REF: dev
   CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/caosdb-mysqlbackend/testenv:latest
@@ -53,6 +53,7 @@ unittests:
     - echo 'DATABASE_USER_HOST_LIST="%,"' >> .config
     - echo "MYSQL_USER_PASSWORD=$MYSQL_ROOT_PASSWORD" >> .config
     - echo "MYSQL_HOST=mariadb" >> .config
+    - sleep 10
     - make install
     - ./utils/make_db restore_db tests/example.dump.sql
     - ./tests/test_utils.sh
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7d3df1f72e4acfdaf4f5232374f7d4f2955e90c8..4dedd48931822c4c9b3c4a20e47410dbbc2b6acb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,16 +10,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added ###
 
+* New `feature_config` table for storing configuration of features as key-value
+  pairs. Currently only used by the ENTITY_VERSIONING feature for switching
+  versioning on or off. Convenient function `is_feature_config` for checking
+  whether the `feature_config` table has a particular value.
+* New `transactions` table. This is necessary for the ENTITY_VERSIONING feature
+  and will replace the `transaction_log` table in the future.
+* Feature ENTITY_VERSIONING (experimental)
+  Switch off this feature with `DELETE FROM feature_config WHERE
+  _key="ENTITY_VERSIONING"` and switch on with `INSERT INTO feature_config
+  (_key, _value) VALUES ("ENTITY_VERSIONING", "ENABLED")`. This feature comes
+  with a lot of additions to the API. E.g.
+  * New `entity_version`.
+  * All `*_data` tables have a new twin, the `archive_*_data` table, where all
+    old versions of entities are stored. The `*_data` tables only contain the
+    data of the latest version.
+  * Additional `archive_isa` for the history entities' parents.
+  * Additional `_iversion` column for the `reference_data` table for storing
+    references to particular versions of an entity.
+  * New `setFileProperties` and `retrieveQueryTemplateDef` procedures which reduce server code and let the
+    backend decide which tables to use. Also, this is necessary for the
+    versioning, because these procedures behave differently depending on the
+    ENTITY_VERSIONING feature being enabled or disabled.
+  * Several functions and procedures for the interaction with the
+    `entity_version` table and the `transactions` table. E.g.
+    `insert_single_child_version`, `delete_all_entity_versions`,
+    `get_iversion`, `get_primary_parent_version`, `get_version_timestamp`,
+    `get_head_version`, `get_head_relative`, `get_version_history`.
+  The versions are tracked internally by the `_iversion` field which is an
+  integer and which should not be used outside of the backend.
+* New makefile targets for testing with MariaDB instance from Docker image: call
+  `make test-docker` to temporarily start a Docker container with MariaDB which
+  will be used for the unit tests.
+
 ### Changed ###
 
+* Removed `getFile` procedure.
+
 ### Deprecated ###
 
+* Table `transaction_log` is deprecated. The functionality is being replaced by the `transactions` table.
+
 ### Fixed ###
 
 * POV with the 'name' property, e.g. `FIND ENTITY WITH name = something`
   [caosdb-server#51](https://gitlab.com/caosdb/caosdb-server/-/issues/51)
-- Fixed several bugs when an Entity inherits from itself (#18, caosdb-server #85).
-- Bug in `updateEntity.sql` (when updating the primary name without a prior call
+* Fixed several bugs when an Entity inherits from itself (#18, caosdb-server #85).
+* Bug in `updateEntity.sql` (when updating the primary name without a prior call
   to `deleteEntityProperties`). Same thing for `deleteEntity`.
 
 ### Security ###
diff --git a/README_SETUP.md b/README_SETUP.md
index 740734a4ca58474ae2102d8d8dc84aaccad84f5e..6a28c66f2dd95483656da8544414d9b43deb88c2 100644
--- a/README_SETUP.md
+++ b/README_SETUP.md
@@ -36,6 +36,40 @@
   be lost afterwards.** And no, there is *no* additional prompt to ask if you
   are sure.  If you `make drop-...`, you *actually* delete the database.
 
+## Versioning
+
+The versioning feature is still experimental. Therefore it is possible to turn
+if on and off with a patch file and a special property.
+
+### Procedures and Functions
+
+The procedures which need to behave differently if the versioning is on or off
+check the return value of `is_feature_config("ENTITY_VERSIONING", "ENABLED")`.
+The `is_feature_config` function checks the `feature_config` table, which is
+a key-value store.
+
+Turn off versioning: Run `UPDATE feature_config SET _value = "DISABLED" WHERE
+_key = "ENTITY_VERSIONING";` on your database.
+
+Turn on versioning again: Run `UPDATE feature_config SET _value = "ENABLED"
+WHERE _key = "ENTITY_VERSIONING";` on your database.
+
+### Data
+
+When the versioning patch is installed, the versioning is turned on by default
+and all old entities become versioned entities with their current version as
+the oldest known version. That is, they all need an entry in the
+`entity_version` table. These entries are generated by the `_fix_unversioned`
+procedure.
+
+If you want to turn off the versioning for the time being you can just turn it
+off for the procedures as described above. You should also empty the
+`entity_version` table because the `_fix_unversioned` procedure is only
+designed to cope with entities which do not have any versioning information at
+all. The already recorded versioning information is of course lost then! If you
+switch on the versioning at some point in the future, the history begins anew
+with the then current version of the stored entities.
+
 ## Unit tests
 
 * We use [MyTAP-1.0](https://hepabolu.github.io/mytap/) for unit tests.
diff --git a/doc/table_structure.md b/doc/table_structure.md
index 3ec27dc6da12b223eff5a5e7b1725b1af91e97cc..59958f5ff1b97caf9c791a1fbb5efce8fa2ca930 100644
--- a/doc/table_structure.md
+++ b/doc/table_structure.md
@@ -56,6 +56,20 @@ These tables share (generally) the same set of columns:
 - `pidx` :: The property index, becomes nonzero when a property occurs more than
   once in an entity.
 
+### `name_data` ###
+
+This table is used to name RecordTypoes, Records, Properties etc.  The column `property_id` has the
+value 20, because that is the entity ID for names:
+
+```sql
+> SELECT * FROM entities WHERE id=20;
++----+-------------------+----------+------+
+| id | description       | role     | acl  |
++----+-------------------+----------+------+
+| 20 | Name of an entity | PROPERTY |    0 |
++----+-------------------+----------+------+
+```
+
 ## data_type ##
 The type of properties is stored here.  The columns are:
 - `domain_id` :: Property identitification, same as for plain data?
@@ -71,6 +85,56 @@ The type of properties is stored here.  The columns are:
 - `hash` :: Hash of the file contents, as binary. **TODO** Which algorithm?
 - `checked_timestamp` :: Timestamp when last checked? 
 
+## `feature_config` ##
++-------------------+---------+
+| _key              | _value  |
++-------------------+---------+
+| ENTITY_VERSIONING | ENABLED |
++-------------------+---------+
+
+A key-value store for features.  Use the `is_feature_config` procedure to check for the status of
+specific features.
+
+## Versioning tables ##
+
+There are a few extra tables for implementing versioning.  Also there is a special column
+`value_iversion` in the `reference_data` table which stores the version of the referenced entity at
+the time of the version.  If `value_iversion` is NULL, no specific version is stored.
+
+### transactions ###
+
+Replacement for `transaction_log`, holds ID, user and time of transaction:
+
+- `srid` :: Server request ID, used to identify transactions
+- `username` :: User name
+- `realm` :: Realm for which the user name is valid
+- `seconds` :: Time of transaction: seconds
+- `nanos` :: Time of transaction: sub-second time resolution
+
+### `entity_version` ###
+
+Versioning info for entities:
+
+- `entity_id` :: persistent ID of the entity
+- `hash` :: Hash, for future use.
+- `version` :: External version string, may be globally unique.  Should be used by API calls.
+- `_iversion` :: Version ID for this entity for internal use, typically an incremental counter,
+  starting with 1
+- `_ipparent` :: Primary parent ID (internal) for this version, i.e. predecessor
+- `srid` :: Server request / transaction ID which created this version
+
+### `archive_foo_data` ###
+
+Older (i.e. not current) data value, from previous versions of an entity.
+- `domain_id` :: Same as in `foo_data`.
+- `entity_id` :: Same as in `foo_data`.
+- `property_id` :: Same as in `foo_data`.
+- `value` :: The value at the given version.
+- `status` :: The value at the given version.
+- `pidx` :: Same as in `foo_data`.
+- `_iversion` :: Version index of this version.
+- ... :: Data type specific columns may also be there.
+
 ## TODO ##
 
 - entity_acl
diff --git a/makefile b/makefile
index 0c9ddabafadaacf14a122ea1c0df9463558219ac..ddac9eda131f804f694ac99af1f495637af3a0aa 100644
--- a/makefile
+++ b/makefile
@@ -51,3 +51,21 @@ drop-%:
 .PHONY: test
 test:
 	./utils/make_db test --fresh
+
+# Run tests with a database which is started in a Docker container
+.PHONY: test-docker
+test-docker:
+	@docker kill caosdb-mysqlserver-test || true
+	@docker container rm caosdb-mysqlserver-test || true
+	@docker run --name caosdb-mysqlserver-test -p "3306:3306" \
+		-e MYSQL_ROOT_PASSWORD="pass-for-test" -d mariadb
+	@sleep 10
+	MAINPATH=$(realpath tests/docker_env) utils/make_db test --fresh
+	@docker kill caosdb-mysqlserver-test
+	@docker container rm caosdb-mysqlserver-test
+
+# if automatic stopping failed
+.PHONY: test-docker-stop
+test-docker-stop:
+	docker kill caosdb-mysqlserver-test
+	docker container rm caosdb-mysqlserver-test
diff --git a/patches/patch20200710-3.0.0-rc2/feature_config.sql b/patches/patch20200710-3.0.0-rc2/feature_config.sql
new file mode 100644
index 0000000000000000000000000000000000000000..4ae95932c94369b17745a2b787f3e4be564b1be9
--- /dev/null
+++ b/patches/patch20200710-3.0.0-rc2/feature_config.sql
@@ -0,0 +1,40 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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 a fresh `feature_config` table and the `is_feature_config` function.
+DROP TABLE IF EXISTS feature_config;
+CREATE TABLE feature_config (
+    _key VARCHAR(255) PRIMARY KEY,
+    _value VARCHAR(255)
+) ENGINE=InnoDB;
+
+delimiter //
+
+DROP FUNCTION IF EXISTS is_feature_config //
+CREATE FUNCTION is_feature_config(
+    _Key VARCHAR(255),
+    Expected VARCHAR(255))
+RETURNS BOOLEAN
+READS SQL DATA
+BEGIN
+    RETURN (
+        SELECT f._value = Expected FROM feature_config as f WHERE f._key = _Key
+    );
+END //
diff --git a/patches/patch20200710-3.0.0-rc2/fix_unversioned.sql b/patches/patch20200710-3.0.0-rc2/fix_unversioned.sql
new file mode 100644
index 0000000000000000000000000000000000000000..4ed6f171264af0c848764867fca8754daf4c62cd
--- /dev/null
+++ b/patches/patch20200710-3.0.0-rc2/fix_unversioned.sql
@@ -0,0 +1,110 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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/>.
+ */
+
+DROP PROCEDURE IF EXISTS _fix_unversioned;
+delimiter //
+
+/*
+ * Generate entries in the entity_version table for all entities which are
+ * stored in the database and have no entry in the entity_version table yet.
+ */
+CREATE PROCEDURE _fix_unversioned()
+BEGIN
+    DECLARE done INT DEFAULT FALSE;
+    DECLARE unversioned_id INT UNSIGNED;
+    -- cursor for unversioned entities
+    DECLARE entities_cur CURSOR FOR SELECT e.id
+        FROM entities AS e
+        WHERE NOT EXISTS (
+            SELECT 1 FROM entity_version AS v WHERE v.entity_id = e.id);
+    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
+
+
+    OPEN entities_cur;
+    insert_loop: LOOP
+        FETCH entities_cur INTO unversioned_id;
+        IF done THEN
+            LEAVE insert_loop;
+        END IF;
+
+        -- create an entry in transaction from the latest log in
+        -- transaction_log, hence the latest update of an entity
+        INSERT IGNORE INTO transactions (srid, username, realm, seconds, nanos)
+            SELECT tmp.srid, tmp.username, tmp.realm, tmp.seconds, tmp.nanos FROM (
+                SELECT
+                    -- SRID needs to be invented in this case, because original server request ID is
+                    -- not known (any more).
+                    SHA2(CONCAT(username, realm, seconds, nanos), 512) AS srid,
+                    username AS username,
+                    realm AS realm,
+                    seconds AS seconds,
+                    nanos AS nanos
+                FROM transaction_log
+                WHERE entity_id = unversioned_id
+                UNION SELECT
+                    -- this is the fallback for the standard entities (id<100)
+                    SHA2("", 512) AS srid,
+                    "administration" As username,
+                    "CaosDB" AS realm,
+                    0 as seconds,
+                    0 as nanos
+            ) AS tmp ORDER BY tmp.seconds DESC, tmp.nanos DESC LIMIT 1; -- only latest transaction
+
+        -- insert single (latest) version into entity_version
+        INSERT INTO entity_version (entity_id, hash, version, _iversion,
+            _ipparent, srid)
+            SELECT tmp.entity_id, tmp.hash, tmp.version, tmp._iversion,
+                tmp._ipparent, tmp.srid
+                FROM (
+                    SELECT
+                        l.entity_id AS entity_id,
+                        NULL as hash,
+                        SHA1(UUID()) as version,
+                        1 AS _iversion,
+                        NULL AS _ipparent,
+                        t.srid AS srid,
+                        t.seconds AS seconds,
+                        t.nanos AS nanos
+                    FROM transactions AS t JOIN transaction_log as l
+                    ON ( t.seconds = l.seconds
+                        AND t.nanos = l.nanos
+                        AND t.username = l.username
+                        AND t.realm  = l.realm )
+                    WHERE l.entity_id = unversioned_id AND unversioned_id >= 100
+                    UNION SELECT
+                        unversioned_id AS entity_id,
+                        NULL AS hash,
+                        SHA1(UUID()) AS version,
+                        1 AS _iversion,
+                        NULL AS _ipparent,
+                        t.srid AS srid,
+                        t.seconds AS seconds,
+                        t.nanos AS nanos
+                    FROM transactions AS t
+                    WHERE t.seconds = 0 AND t.nanos = 0 AND unversioned_id < 100
+                ) AS tmp ORDER BY tmp.seconds DESC, tmp.nanos DESC LIMIT 1;
+
+
+    END LOOP;
+    CLOSE entities_cur;
+END;
+//
+
+delimiter ;
diff --git a/patches/patch20200710-3.0.0-rc2/patch.sh b/patches/patch20200710-3.0.0-rc2/patch.sh
new file mode 100755
index 0000000000000000000000000000000000000000..9989b531bf60fe58d57a47d0b40f97df725fe14a
--- /dev/null
+++ b/patches/patch20200710-3.0.0-rc2/patch.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+#
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2018 Research Group Biomedical Physics,
+# Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+# Copyright (C) 2020 Timm Fitschen <t.fitschen@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/>.
+#
+# ** end header
+#
+# new entity_version table
+# Update mysql schema to version v3.0.0-rc2
+NEW_VERSION="v3.0.0-rc2"
+OLD_VERSION="v3.0.0-rc1"
+
+if [ -z "$UTILSPATH" ]; then
+ UTILSPATH="../utils"
+fi
+
+. $UTILSPATH/patch_header.sh $*
+
+
+check_version $OLD_VERSION
+
+
+mysql_execute_file $PATCH_DIR/feature_config.sql
+mysql_execute_file $PATCH_DIR/versioning.sql
+
+mysql_execute_file $PATCH_DIR/fix_unversioned.sql
+mysql_execute "CALL _fix_unversioned()"
+mysql_execute "DROP PROCEDURE _fix_unversioned"
+
+
+update_version $NEW_VERSION
+
+success
+
diff --git a/patches/patch20200710-3.0.0-rc2/versioning.sql b/patches/patch20200710-3.0.0-rc2/versioning.sql
new file mode 100644
index 0000000000000000000000000000000000000000..9f298979b1bb5df904b6e163b9d2ad92f79c243a
--- /dev/null
+++ b/patches/patch20200710-3.0.0-rc2/versioning.sql
@@ -0,0 +1,320 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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 tables for versioning.
+DROP TABLE IF EXISTS archive_reference_data;
+DROP TABLE IF EXISTS archive_null_data;
+DROP TABLE IF EXISTS archive_text_data;
+DROP TABLE IF EXISTS archive_name_data;
+DROP TABLE IF EXISTS archive_enum_data;
+DROP TABLE IF EXISTS archive_integer_data;
+DROP TABLE IF EXISTS archive_double_data;
+DROP TABLE IF EXISTS archive_datetime_data;
+DROP TABLE IF EXISTS archive_date_data;
+DROP TABLE IF EXISTS archive_name_overrides;
+DROP TABLE IF EXISTS archive_desc_overrides;
+DROP TABLE IF EXISTS archive_data_type;
+DROP TABLE IF EXISTS archive_collection_type;
+DROP TABLE IF EXISTS archive_query_template_def;
+DROP TABLE IF EXISTS archive_files;
+DROP TABLE IF EXISTS archive_entities;
+DROP TABLE IF EXISTS archive_isa;
+DROP TABLE IF EXISTS entity_version;
+DROP TABLE IF EXISTS transactions;
+
+CREATE TABLE transactions (
+  srid VARBINARY(255) PRIMARY KEY,  -- server request ID ?
+  username VARBINARY(255) NOT NULL, -- who did the transactions: username
+  realm VARBINARY(255) NOT NULL,    -- who did the transactions: realm
+  seconds BIGINT UNSIGNED NOT NULL, -- time of transaction: seconds
+  nanos INT(10) UNSIGNED NOT NULL   -- time of transaction: sub-second time resolution
+) ENGINE=InnoDB;
+
+-- TODO remove ON DELETE CASCADE when feature is stable.
+CREATE TABLE entity_version (
+  entity_id INT UNSIGNED NOT NULL,
+  hash VARBINARY(255) DEFAULT NULL,
+  version VARBINARY(255) NOT NULL, -- external version identifier, may be globally unique
+  _iversion INT UNSIGNED NOT NULL, -- internal version ID, typically an incremental counter
+  _ipparent INT UNSIGNED NULL,  -- (internal) ID of the primary parent, i.e. of the predecessor
+  srid VARBINARY(255) NOT NULL,
+  PRIMARY KEY (`entity_id`, `_iversion`),
+  FOREIGN KEY (`entity_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+  FOREIGN KEY (`srid`) REFERENCES `transactions` (`srid`),
+  UNIQUE `entity_version-e-v` (`entity_id`, `version`)
+) ENGINE=InnoDB;
+
+ALTER TABLE reference_data
+    ADD COLUMN `value_iversion` INT UNSIGNED DEFAULT NULL,
+    ADD FOREIGN KEY (`value`, `value_iversion`) REFERENCES
+        entity_version (`entity_id`, `_iversion`);
+
+
+CREATE TABLE archive_isa (
+  child INT UNSIGNED NOT NULL,
+  child_iversion INT UNSIGNED NOT NULL,
+  parent INT UNSIGNED NOT NULL,
+  FOREIGN KEY (`parent`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+  FOREIGN KEY (`child`, `child_iversion`) REFERENCES `entity_version`
+      (`entity_id`, `_iversion`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_reference_data (
+    domain_id INT UNSIGNED NOT NULL,
+    entity_id INT UNSIGNED NOT NULL,
+    property_id INT UNSIGNED NOT NULL,
+    value INT UNSIGNED NOT NULL,
+    value_iversion INT UNSIGNED DEFAULT NULL,
+    status ENUM('OBLIGATORY','RECOMMENDED','SUGGESTED','FIX','REPLACEMENT')
+        NOT NULL,
+    pidx INT(10) UNSIGNED NOT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    KEY (`domain_id`, `entity_id`, `_iversion`),
+    KEY (`domain_id`, `_iversion`),
+    FOREIGN KEY (`domain_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`entity_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`property_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`value`) REFERENCES `entities` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_null_data (
+    domain_id INT UNSIGNED NOT NULL,
+    entity_id INT UNSIGNED NOT NULL,
+    property_id INT UNSIGNED NOT NULL,
+    status ENUM('OBLIGATORY','RECOMMENDED','SUGGESTED','FIX')
+        NOT NULL,
+    pidx INT(10) UNSIGNED NOT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    KEY (`domain_id`, `entity_id`, `_iversion`),
+    KEY (`domain_id`, `_iversion`),
+    FOREIGN KEY (`domain_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`entity_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`property_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_text_data (
+    domain_id INT UNSIGNED NOT NULL,
+    entity_id INT UNSIGNED NOT NULL,
+    property_id INT UNSIGNED NOT NULL,
+    value TEXT NOT NULL,
+    status ENUM('OBLIGATORY','RECOMMENDED','SUGGESTED','FIX')
+        NOT NULL,
+    pidx INT(10) UNSIGNED NOT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    KEY (`domain_id`, `entity_id`, `_iversion`),
+    KEY (`domain_id`, `_iversion`),
+    FOREIGN KEY (`domain_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`entity_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`property_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_name_data (
+    domain_id INT UNSIGNED NOT NULL,
+    entity_id INT UNSIGNED NOT NULL,
+    property_id INT UNSIGNED NOT NULL,
+    value VARCHAR(255) NOT NULL,
+    status ENUM('OBLIGATORY','RECOMMENDED','SUGGESTED','FIX')
+        NOT NULL,
+    pidx INT(10) UNSIGNED NOT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    KEY (`domain_id`, `entity_id`, `_iversion`),
+    KEY (`domain_id`, `_iversion`),
+    KEY (`value`),
+    FOREIGN KEY (`domain_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`entity_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`property_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_enum_data (
+    domain_id INT UNSIGNED NOT NULL,
+    entity_id INT UNSIGNED NOT NULL,
+    property_id INT UNSIGNED NOT NULL,
+    value VARBINARY(255) NOT NULL,
+    status ENUM('OBLIGATORY','RECOMMENDED','SUGGESTED','FIX')
+        NOT NULL,
+    pidx INT(10) UNSIGNED NOT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    KEY (`domain_id`, `entity_id`, `_iversion`),
+    KEY (`domain_id`, `_iversion`),
+    FOREIGN KEY (`domain_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`entity_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`property_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_integer_data (
+    domain_id INT UNSIGNED NOT NULL,
+    entity_id INT UNSIGNED NOT NULL,
+    property_id INT UNSIGNED NOT NULL,
+    value BIGINT NOT NULL,
+    status ENUM('OBLIGATORY','RECOMMENDED','SUGGESTED','FIX')
+        NOT NULL,
+    pidx INT(10) UNSIGNED NOT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    unit_sig BIGINT DEFAULT NULL,
+    KEY (`domain_id`, `entity_id`, `_iversion`),
+    KEY (`domain_id`, `_iversion`),
+    FOREIGN KEY (`domain_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`entity_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`property_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_double_data (
+    domain_id INT UNSIGNED NOT NULL,
+    entity_id INT UNSIGNED NOT NULL,
+    property_id INT UNSIGNED NOT NULL,
+    value DOUBLE NOT NULL,
+    status ENUM('OBLIGATORY','RECOMMENDED','SUGGESTED','FIX')
+        NOT NULL,
+    pidx INT(10) UNSIGNED NOT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    unit_sig BIGINT DEFAULT NULL,
+    KEY (`domain_id`, `entity_id`, `_iversion`),
+    KEY (`domain_id`, `_iversion`),
+    FOREIGN KEY (`domain_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`entity_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`property_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_datetime_data (
+    domain_id INT UNSIGNED NOT NULL,
+    entity_id INT UNSIGNED NOT NULL,
+    property_id INT UNSIGNED NOT NULL,
+    value BIGINT NOT NULL,
+    value_ns INT(10) UNSIGNED DEFAULT NULL,
+    status ENUM('OBLIGATORY','RECOMMENDED','SUGGESTED','FIX')
+        NOT NULL,
+    pidx INT(10) UNSIGNED NOT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    KEY (`domain_id`, `entity_id`, `_iversion`),
+    KEY (`domain_id`, `_iversion`),
+    FOREIGN KEY (`domain_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`entity_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`property_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_date_data (
+    domain_id INT UNSIGNED NOT NULL,
+    entity_id INT UNSIGNED NOT NULL,
+    property_id INT UNSIGNED NOT NULL,
+    value INT(11) NOT NULL,
+    status ENUM('OBLIGATORY','RECOMMENDED','SUGGESTED','FIX')
+        NOT NULL,
+    pidx INT(10) UNSIGNED NOT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    KEY (`domain_id`, `entity_id`, `_iversion`),
+    KEY (`domain_id`, `_iversion`),
+    FOREIGN KEY (`domain_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`entity_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`property_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_name_overrides (
+    domain_id INT UNSIGNED NOT NULL,
+    entity_id INT UNSIGNED NOT NULL,
+    property_id INT UNSIGNED NOT NULL,
+    name VARCHAR(255) NOT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    UNIQUE KEY `archive_name_overrides-d-e-p-v` (`domain_id`, `entity_id`, `property_id`, `_iversion`),
+    KEY (`domain_id`, `entity_id`, `_iversion`),
+    KEY (`domain_id`, `_iversion`),
+    FOREIGN KEY (`domain_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`entity_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`property_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_desc_overrides (
+    domain_id INT UNSIGNED NOT NULL,
+    entity_id INT UNSIGNED NOT NULL,
+    property_id INT UNSIGNED NOT NULL,
+    description TEXT NOT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    UNIQUE KEY `archive_desc_overrides-d-e-p-v` (`domain_id`, `entity_id`, `property_id`, `_iversion`),
+    KEY (`domain_id`, `entity_id`, `_iversion`),
+    KEY (`domain_id`, `_iversion`),
+    FOREIGN KEY (`domain_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`entity_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`property_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_data_type (
+    domain_id INT UNSIGNED NOT NULL,
+    entity_id INT UNSIGNED NOT NULL,
+    property_id INT UNSIGNED NOT NULL,
+    datatype INT UNSIGNED NOT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    UNIQUE KEY `archive_data_type-d-e-p-v` (`domain_id`, `entity_id`, `property_id`, `_iversion`),
+    KEY (`domain_id`, `entity_id`, `_iversion`),
+    KEY (`domain_id`, `_iversion`),
+    FOREIGN KEY (`domain_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`entity_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`property_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`datatype`) REFERENCES `entities` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_collection_type (
+    domain_id INT UNSIGNED NOT NULL,
+    entity_id INT UNSIGNED NOT NULL,
+    property_id INT UNSIGNED NOT NULL,
+    collection VARCHAR(255) NOT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    UNIQUE KEY `archive_collection_type-d-e-p-v` (`domain_id`, `entity_id`, `property_id`, `_iversion`),
+    KEY (`domain_id`, `entity_id`, `_iversion`),
+    KEY (`domain_id`, `_iversion`),
+    FOREIGN KEY (`domain_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`entity_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
+    FOREIGN KEY (`property_id`) REFERENCES `entities` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_query_template_def (
+    id INT UNSIGNED NOT NULL,
+    definition MEDIUMTEXT NOT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    PRIMARY KEY (`id`, `_iversion`),
+    FOREIGN KEY (`id`, `_iversion`)
+        REFERENCES `entity_version` (`entity_id`, `_iversion`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_files (
+    file_id INT UNSIGNED NOT NULL,
+    `path` TEXT NOT NULL,
+    size BIGINT UNSIGNED NOT NULL,
+    hash BINARY(64) DEFAULT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    PRIMARY KEY (`file_id`, `_iversion`),
+    FOREIGN KEY (`file_id`, `_iversion`)
+        REFERENCES `entity_version` (`entity_id`, `_iversion`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE archive_entities (
+    id INT UNSIGNED NOT NULL,
+    description TEXT DEFAULT NULL,
+    role ENUM('RECORDTYPE','RECORD','FILE','DOMAIN',
+        'PROPERTY','DATATYPE','ROLE','QUERYTEMPLATE') NOT NULL,
+    acl INT(10) UNSIGNED DEFAULT NULL,
+    _iversion INT UNSIGNED NOT NULL,
+    PRIMARY KEY (`id`, `_iversion`),
+    FOREIGN KEY (`id`, `_iversion`)
+        REFERENCES `entity_version` (`entity_id`, `_iversion`) ON DELETE CASCADE,
+    FOREIGN KEY (`acl`) REFERENCES `entity_acl` (`id`)
+) ENGINE=InnoDB;
+
+ALTER IGNORE TABLE collection_type ADD UNIQUE KEY `collection_type-d-e-p` (`domain_id`, `entity_id`, `property_id`);
+
+INSERT INTO feature_config (_key, _value) VALUES ("ENTITY_VERSIONING", "ENABLED");
diff --git a/procedures/deleteEntity.sql b/procedures/deleteEntity.sql
index b96289bc58b77afea9e5ca927760de905eddbc0e..d66a7d7c22e290935a59e944e9200d3ad5909b4e 100644
--- a/procedures/deleteEntity.sql
+++ b/procedures/deleteEntity.sql
@@ -45,14 +45,32 @@ BEGIN
     DELETE FROM files where file_id=EntityID;
 
     -- delete datatype stuff
-    DELETE FROM data_type WHERE domain_id=0 and (entity_id=0 and property_id=EntityID) or entity_id=EntityID; 
-    DELETE FROM collection_type WHERE domain_id=0 and (entity_id=0 and property_id=EntityID) or entity_id=EntityID; 
+    DELETE FROM data_type
+        WHERE ( domain_id = 0
+            AND entity_id = 0
+            AND property_id = EntityID )
+        OR datatype = EntityID;
+    DELETE FROM collection_type
+        WHERE domain_id = 0
+        AND entity_id = 0
+        AND property_id = EntityID;
 
     -- delete primary name (in case this is called without a prior call to deleteEntityProperties)
-    DELETE FROM name_data WHERE domain_id = 0 AND entity_id = EntityID AND property_id = 20;
+    DELETE FROM name_data
+        WHERE domain_id = 0
+        AND entity_id = EntityID
+        AND property_id = 20;
 
     DELETE FROM entities where id=EntityID;
-    DELETE FROM entity_acl WHERE NOT EXISTS (SELECT 1 FROM entities WHERE entities.acl = entity_acl.id LIMIT 1);
+
+    -- clean up unused acl
+    DELETE FROM entity_acl
+        WHERE NOT EXISTS (
+            SELECT 1 FROM entities
+            WHERE entities.acl = entity_acl.id LIMIT 1)
+        AND NOT EXISTS (
+            SELECT 1 FROM archive_entities
+            WHERE archive_entities.acl = entity_acl.id LIMIT 1);
 
 END;
 //
diff --git a/procedures/deleteEntityProperties.sql b/procedures/deleteEntityProperties.sql
index 2e8b7faf146c10701b45adbdf89b99a5448362c8..2075696ef244d54dffd6d658adf0a74cb8662179 100644
--- a/procedures/deleteEntityProperties.sql
+++ b/procedures/deleteEntityProperties.sql
@@ -4,6 +4,8 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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
@@ -26,37 +28,157 @@ delimiter //
 
 CREATE PROCEDURE db_2_0.deleteEntityProperties(in EntityID INT UNSIGNED)
 BEGIN
+    DECLARE IVersion INT UNSIGNED DEFAULT NULL;
 
-CALL deleteIsa(EntityID);
-
-DELETE FROM reference_data 
-where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
-DELETE FROM null_data 
-where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
-DELETE FROM text_data 
-where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
-DELETE FROM name_data 
-where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
-DELETE FROM enum_data 
-where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
-DELETE FROM integer_data 
-where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
-DELETE FROM double_data 
-where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
-DELETE FROM datetime_data 
-where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
-DELETE FROM date_data 
-where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
-DELETE FROM name_overrides
-WHERE (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
-DELETE FROM desc_overrides
-WHERE (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
-DELETE FROM collection_type
-WHERE (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
-DELETE FROM data_type 
-WHERE (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID OR (domain_id=0 AND entity_id=0 AND property_id=EntityID);
-
-DELETE FROM query_template_def WHERE id=EntityID;
+    CALL deleteIsa(EntityID);
+
+    IF is_feature_config("ENTITY_VERSIONING", "ENABLED") THEN
+        SELECT max(e._iversion) INTO IVersion -- What's the latest version?
+            FROM entity_version AS e
+            WHERE e.entity_id = EntityID;
+
+        -- Copy the rows from *_data to archive_*_data ---------------------
+        INSERT INTO archive_reference_data (domain_id, entity_id,
+                property_id, value, value_iversion, status, pidx, _iversion)
+            SELECT domain_id, entity_id, property_id, value, value_iversion,
+                status, pidx, IVersion AS _iversion
+            FROM reference_data
+            WHERE (domain_id = 0 AND entity_id = EntityID)
+            OR domain_id = EntityID;
+
+        INSERT INTO archive_null_data (domain_id, entity_id,
+                property_id, status, pidx, _iversion)
+            SELECT domain_id, entity_id, property_id, status,
+                pidx, IVersion AS _iversion
+            FROM null_data
+            WHERE (domain_id = 0 AND entity_id = EntityID)
+            OR domain_id = EntityID;
+
+        INSERT INTO archive_text_data (domain_id, entity_id,
+                property_id, value, status, pidx, _iversion)
+            SELECT domain_id, entity_id, property_id, value, status,
+                pidx, IVersion AS _iversion
+            FROM text_data
+            WHERE (domain_id = 0 AND entity_id = EntityID)
+            OR domain_id = EntityID;
+
+        INSERT INTO archive_name_data (domain_id, entity_id,
+                property_id, value, status, pidx, _iversion)
+            SELECT domain_id, entity_id, property_id, value, status,
+                pidx, IVersion AS _iversion
+            FROM name_data
+            WHERE (domain_id = 0 AND entity_id = EntityID)
+            OR domain_id = EntityID;
+
+        INSERT INTO archive_enum_data (domain_id, entity_id,
+                property_id, value, status, pidx, _iversion)
+            SELECT domain_id, entity_id, property_id, value, status,
+                pidx, IVersion AS _iversion
+            FROM enum_data
+            WHERE (domain_id = 0 AND entity_id = EntityID)
+            OR domain_id = EntityID;
+
+        INSERT INTO archive_integer_data (domain_id, entity_id,
+                property_id, value, status, pidx, _iversion, unit_sig)
+            SELECT domain_id, entity_id, property_id, value, status,
+                pidx, IVersion AS _iversion, unit_sig
+            FROM integer_data
+            WHERE (domain_id = 0 AND entity_id = EntityID)
+            OR domain_id = EntityID;
+
+        INSERT INTO archive_double_data (domain_id, entity_id,
+                property_id, value, status, pidx, _iversion, unit_sig)
+            SELECT domain_id, entity_id, property_id, value, status,
+                pidx, IVersion AS _iversion, unit_sig
+            FROM double_data
+            WHERE (domain_id = 0 AND entity_id = EntityID)
+            OR domain_id = EntityID;
+
+        INSERT INTO archive_datetime_data (domain_id, entity_id,
+                property_id, value, value_ns, status, pidx, _iversion)
+            SELECT domain_id, entity_id, property_id, value, value_ns,
+                status, pidx, IVersion AS _iversion
+            FROM datetime_data
+            WHERE (domain_id = 0 AND entity_id = EntityID)
+            OR domain_id = EntityID;
+
+        INSERT INTO archive_date_data (domain_id, entity_id,
+                property_id, value, status, pidx, _iversion)
+            SELECT domain_id, entity_id, property_id, value, status,
+                pidx, IVersion AS _iversion
+            FROM date_data
+            WHERE (domain_id = 0 AND entity_id = EntityID)
+            OR domain_id = EntityID;
+
+        INSERT INTO archive_name_overrides (domain_id, entity_id,
+                property_id, name, _iversion)
+            SELECT domain_id, entity_id, property_id, name,
+                IVersion AS _iversion
+            FROM name_overrides
+            WHERE (domain_id = 0 AND entity_id = EntityID)
+            OR domain_id = EntityID;
+
+        INSERT INTO archive_desc_overrides (domain_id, entity_id,
+                property_id, description, _iversion)
+            SELECT domain_id, entity_id, property_id, description,
+                IVersion AS _iversion
+            FROM desc_overrides
+            WHERE (domain_id = 0 AND entity_id = EntityID)
+            OR domain_id = EntityID;
+
+        INSERT INTO archive_data_type (domain_id, entity_id,
+                property_id, datatype, _iversion)
+            SELECT domain_id, entity_id, property_id, datatype,
+                IVersion AS _iversion
+            FROM data_type
+            WHERE (domain_id = 0 AND entity_id = EntityID)
+            OR domain_id = EntityID;
+
+        INSERT INTO archive_collection_type (domain_id, entity_id,
+                property_id, collection, _iversion)
+            SELECT domain_id, entity_id, property_id, collection,
+                IVersion AS _iversion
+            FROM collection_type
+            WHERE (domain_id = 0 AND entity_id = EntityID)
+            OR domain_id = EntityID;
+
+        INSERT INTO archive_query_template_def (id, definition, _iversion)
+            SELECT id, definition, IVersion AS _iversion
+            FROM query_template_def
+            WHERE id = EntityID;
+
+    END IF;
+
+    DELETE FROM reference_data
+    where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
+    DELETE FROM null_data
+    where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
+    DELETE FROM text_data
+    where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
+    DELETE FROM name_data
+    where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
+    DELETE FROM enum_data
+    where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
+    DELETE FROM integer_data
+    where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
+    DELETE FROM double_data
+    where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
+    DELETE FROM datetime_data
+    where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
+    DELETE FROM date_data
+    where (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
+
+    DELETE FROM name_overrides
+    WHERE (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
+    DELETE FROM desc_overrides
+    WHERE (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
+
+    DELETE FROM data_type
+    WHERE (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
+    DELETE FROM collection_type
+    WHERE (domain_id=0 AND entity_id=EntityID) OR domain_id=EntityID;
+
+    DELETE FROM query_template_def WHERE id=EntityID;
 
 END;
 //
diff --git a/procedures/deleteIsaCache.sql b/procedures/deleteIsaCache.sql
index 6713d4a2ffa90f00b973e986ac0c4647f00a7a61..f37c3fc8d4f7d7eeeff3ad651cf7b5ab0864d20f 100644
--- a/procedures/deleteIsaCache.sql
+++ b/procedures/deleteIsaCache.sql
@@ -5,6 +5,7 @@
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
  * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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
@@ -41,8 +42,26 @@ Child entity for which all parental relations should be deleted.
 */
 CREATE PROCEDURE db_2_0.deleteIsa(IN EntityID INT UNSIGNED)
 BEGIN
-	
-	DELETE FROM isa_cache WHERE child=EntityID or rpath=EntityID or rpath LIKE concat('%>',EntityID) or rpath LIKE concat('%>', EntityID, '>%');
+    DECLARE IVersion INT UNSIGNED DEFAULT NULL;
+
+    IF is_feature_config("ENTITY_VERSIONING", "ENABLED") THEN
+        SELECT max(_iversion) INTO IVersion
+            FROM entity_version
+            WHERE entity_id = EntityID;
+
+        -- move to archive_isa before deleting (only direct child-parent relations)
+        INSERT INTO archive_isa (child, child_iversion, parent)
+            SELECT e.child, IVersion AS child_iversion, e.parent
+            FROM isa_cache AS e
+            WHERE e.child = EntityID
+            AND e.rpath = EntityID;
+    END IF;
+
+    DELETE FROM isa_cache
+        WHERE child = EntityID
+        OR rpath = EntityID
+        OR rpath LIKE concat('%>', EntityID)
+        OR rpath LIKE concat('%>', EntityID, '>%');
 
 END;
 //
diff --git a/procedures/entityACL.sql b/procedures/entityACL.sql
index a4a0c2d09a055ffe0bc776cc45f4f6967c8dd003..5f5822b583566f4c2222991a651eb02d3795904e 100644
--- a/procedures/entityACL.sql
+++ b/procedures/entityACL.sql
@@ -28,8 +28,8 @@ CREATE PROCEDURE db_2_0.entityACL(out ACLID INT UNSIGNED, in ACLSTR VARBINARY(65
 BEGIN
    SELECT id INTO ACLID FROM entity_acl as t WHERE t.acl=ACLSTR LIMIT 1;
    IF ACLID IS NULL THEN
-		INSERT INTO entity_acl (acl) VALUES (ACLSTR);
-		SET ACLID = LAST_INSERT_ID();
+        INSERT INTO entity_acl (acl) VALUES (ACLSTR);
+        SET ACLID = LAST_INSERT_ID();
    END IF;
 END;
 //
diff --git a/procedures/entityVersioning.sql b/procedures/entityVersioning.sql
new file mode 100644
index 0000000000000000000000000000000000000000..b009d3f2ab8dd58ecb2199e57c40b99419007a0d
--- /dev/null
+++ b/procedures/entityVersioning.sql
@@ -0,0 +1,487 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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/>.
+ */
+
+delimiter //
+
+DROP PROCEDURE IF EXISTS db_2_0.insert_single_child_version //
+
+/**
+ * Creates a new version record in the `entity_version` table.
+ *
+ * Parameters
+ * ----------
+ * EntityID
+ *   The ID of the versioned entity.
+ * Hash
+ *   A hash of the entity. This is currently not implemented properly and only
+ *   there for future use.
+ * Version
+ *   The new version ID of the entity, must be produced by the caller.  Must be unique for each
+ *   EntityID.
+ * Parent
+ *   The version ID of the primary parent (i.e. predecessor).  May be NULL; but if given, it must
+ *   exist.
+ * Transaction
+ *   The transaction ID which created this entity version (by inserting
+ *   or updating an entity).
+ */
+CREATE PROCEDURE db_2_0.insert_single_child_version(
+    in EntityID INT UNSIGNED,
+    in Hash VARBINARY(255),
+    in Version VARBINARY(255),
+    in Parent VARBINARY(255),
+    in Transaction VARBINARY(255))
+
+BEGIN
+    DECLARE newiversion INT UNSIGNED DEFAULT NULL;
+    DECLARE newipparent INT UNSIGNED DEFAULT NULL;
+
+    -- find _ipparent
+    IF Parent IS NOT NULL THEN
+        SELECT e._iversion INTO newipparent
+            FROM entity_version AS e
+            WHERE e.entity_id = EntityID
+            AND e.version = Parent;
+        IF newipparent IS NULL THEN
+            -- throw error;
+            SELECT concat("This parent does not exists: ", Parent)
+            FROM nonexisting;
+        END IF;
+    END IF;
+
+
+    -- generate _iversion
+    SELECT max(e._iversion)+1 INTO newiversion
+        FROM entity_version AS e
+        WHERE e.entity_id=EntityID;
+    IF newiversion IS NULL THEN
+        SET newiversion = 1;
+    END IF;
+
+    INSERT INTO entity_version
+        (entity_id, hash, version, _iversion, _ipparent, srid)
+        VALUES
+        (EntityID, Hash, Version, newiversion, newipparent, Transaction);
+
+
+
+END;
+//
+
+DROP PROCEDURE IF EXISTS db_2_0.delete_all_entity_versions //
+
+/**
+ * Remove all records in the entity_version table for the given entity.
+ *
+ * Parameters
+ * ----------
+ * EntityID
+ *   The id of the versioned entity.
+ */
+CREATE PROCEDURE db_2_0.delete_all_entity_versions(
+    in EntityID INT UNSIGNED)
+BEGIN
+
+    DELETE FROM entity_version WHERE entity_id = EntityID;
+
+END;
+//
+
+DROP FUNCTION IF EXISTS db_2_0.get_iversion //
+
+/**
+ * Get the internal version id (an integer) of the (API-)version id.
+ *
+ * Parameters
+ * ----------
+ * EntityID
+ *   The entity's id.
+ * Version
+ *   The (official, externally used) version id.
+ *
+ * Returns
+ * -------
+ * The internal version id.
+ */
+CREATE FUNCTION db_2_0.get_iversion(
+    EntityID INT UNSIGNED,
+    Version VARBINARY(255))
+RETURNS INT UNSIGNED
+READS SQL DATA
+BEGIN
+    RETURN (
+        SELECT e._iversion
+            FROM entity_version AS e
+            WHERE e.entity_id = EntityID
+                AND e.version = Version
+        );
+END;
+//
+
+
+DROP FUNCTION IF EXISTS db_2_0.get_primary_parent_version //
+
+/**
+ * Get the version ID of the primary parent (i.e. predecessor) of a version.
+ *
+ * Parameters
+ * ----------
+ * EntityID
+ *   The entity id.
+ * Version
+ *   The version id.
+ *
+ * Returns
+ * -------
+ * The id of the given version's primary parent version.
+ */
+CREATE FUNCTION db_2_0.get_primary_parent_version(
+    EntityID INT UNSIGNED,
+    Version VARBINARY(255))
+RETURNS VARBINARY(255)
+READS SQL DATA
+BEGIN
+    RETURN (
+        SELECT p.version
+            FROM entity_version AS e INNER JOIN entity_version AS p
+                ON (e._ipparent = p._iversion
+                    AND e.entity_id = p.entity_id)
+            WHERE e.entity_id = EntityID
+                AND e.version = Version
+        );
+END;
+//
+
+
+DROP FUNCTION IF EXISTS db_2_0.get_version_timestamp //
+/**
+ * Get the timestamp of a version by retrieving the timestamp of the
+ * transaction which created the version.
+ *
+ * Parameters
+ * ----------
+ * EntityID
+ *   The entity id.
+ * Version
+ *   The version id.
+ *
+ * Returns
+ * -------
+ * The versions' timestamp in <seconds-since-epoch>.<nanoseconds> format.
+ * Note that the dot `.` here is not necessarily a decimal separator.
+ */
+CREATE FUNCTION db_2_0.get_version_timestamp(
+    EntityID INT UNSIGNED,
+    Version VARBINARY(255))
+RETURNS VARCHAR(255)
+READS SQL DATA
+BEGIN
+    RETURN (
+        SELECT concat(t.seconds, '.', t.nanos)
+            FROM entity_version AS e INNER JOIN transactions AS t
+                ON ( e.srid = t.srid )
+            WHERE e.entity_id = EntityID
+            AND e.version = Version
+    );
+END;
+//
+
+DROP FUNCTION IF EXISTS db_2_0.get_head_version //
+
+/**
+ * Get the version id of the HEAD of an entity.
+ *
+ * Parameters
+ * ----------
+ * EntityID
+ *   The entity id.
+ *
+ * Returns
+ * -------
+ * The version id of the HEAD.
+ */
+CREATE FUNCTION db_2_0.get_head_version(
+    EntityID INT UNSIGNED)
+RETURNS VARBINARY(255)
+READS SQL DATA
+BEGIN
+    RETURN get_head_relative(EntityID, 0);
+END;
+//
+
+
+DROP FUNCTION IF EXISTS db_2_0.get_head_relative //
+
+/**
+ * Get the id of a version which is specified relative to the HEAD of an
+ * entity.
+ *
+ * Parameters
+ * ----------
+ * EntityID
+ *   The entity id.
+ * Offset
+ *   Distance in the sequence of primary parents of the entity. E.g. `0` is the
+ *   HEAD itself. `1` is the primary parent of the HEAD. `2` is the primary
+ *   parent of the primary parent of the HEAD, and so on.
+ *
+ * Returns
+ * -------
+ * The version id of the HEAD.
+ */
+CREATE FUNCTION db_2_0.get_head_relative(
+    EntityID INT UNSIGNED,
+    Offset INT UNSIGNED)
+RETURNS VARBINARY(255)
+READS SQL DATA
+BEGIN
+    -- This implementation assumes that the distance from the head equals the
+    -- difference between the _iversion numbers. This will not be correct anymore
+    -- as soon as branches may split and merge. Then, a walk over the primary
+    -- parents (_ipparent) will be necessary.
+    RETURN (
+        SELECT e.version
+            FROM entity_version AS e
+            WHERE e.entity_id = EntityID
+            ORDER BY e._iversion DESC
+            LIMIT 1 OFFSET Offset
+        );
+END;
+//
+
+DROP FUNCTION IF EXISTS db_2_0._get_version //
+/**
+ * The inverse function of get_iversion. Resolves the version id.
+ *
+ * Parameters
+ * ----------
+ * EntityID
+ *   The entity id.
+ * IVersion
+ *   Internal version id (integer).
+ *
+ * Returns
+ * -------
+ * The version id.
+ */
+CREATE FUNCTION db_2_0._get_version(
+    EntityID INT UNSIGNED,
+    IVersion INT UNSIGNED)
+RETURNS VARBINARY(255)
+READS SQL DATA
+BEGIN
+    RETURN (
+        SELECT version FROM entity_version
+            WHERE entity_id = EntityID
+            AND _iversion = IVersion
+        );
+END;
+//
+
+DROP PROCEDURE IF EXISTS db_2_0.get_version_history //
+
+/**
+ * Select version timestamps of an entity.
+ *
+ * Parameters
+ * ----------
+ * EntityID
+ *   The entity id.
+ *
+ * Selects
+ * -------
+ * Tuples (child, parent, child_seconds, child_nanos). `child` and `parent` are
+ * version IDs.
+ */
+CREATE PROCEDURE db_2_0.get_version_history(
+    in EntityID INT UNSIGNED)
+BEGIN
+    -- retrieve root(s) (initial versions)
+    SELECT c.version AS child,
+            NULL as parent,
+            t.seconds AS child_seconds,
+            t.nanos AS child_nanos
+        FROM entity_version AS c INNER JOIN transactions as t
+        ON ( c.srid = t.srid )
+        WHERE c.entity_id = EntityID
+        AND c._ipparent is Null
+
+    -- TODO This first SELECT statement is necessary because the second one
+    -- does not return the root. However, this should be doable in one go with
+    -- a left join instead of an inner join(?).
+
+    -- retrieve branches (later versions)
+    UNION SELECT c.version AS child,
+            p.version AS parent,
+            t.seconds AS child_seconds,
+            t.nanos AS child_nanos
+        FROM entity_version AS p
+            INNER JOIN entity_version as c
+            INNER JOIN transactions AS t
+            ON (c._ipparent = p._iversion
+                AND c.entity_id = p.entity_id
+                AND t.srid = c.srid)
+        WHERE p.entity_id = EntityID;
+END;
+//
+
+
+DROP PROCEDURE IF EXISTS set_transaction //
+/**
+ * Called when a new transaction begins. Inserts the transaction metadata into
+ * the transactions table.
+ *
+ * Parameters
+ * ----------
+ * srid
+ *   The server-generated Server Request ID.
+ * username
+ *   The username of the subject who/which is responsible for the transaction.
+ * realm
+ *   The realm of the subject.
+ * seconds
+ *   Unix timestamp (seconds).
+ * nanos
+ *   Nanoseconds precision for the timestamp.
+ */
+CREATE PROCEDURE set_transaction(
+    srid VARBINARY(255),
+    username VARCHAR(255),
+    realm VARCHAR(255),
+    seconds BIGINT UNSIGNED,
+    nanos INT(10) UNSIGNED)
+BEGIN
+
+    SET @SRID = srid;  -- Make the transaction / server request ID globally available.
+    INSERT INTO transactions (srid, username, realm, seconds, nanos)
+        VALUES (srid, username, realm, seconds, nanos);
+END //
+
+
+
+DROP PROCEDURE IF EXISTS setFileProperties //
+/**
+ * Insert/Update file properties.
+ *
+ * If ENTITY_VERSIONING is enabled the old file properties are moved to
+ * `archive_files`.
+ *
+ * Parameters
+ * ----------
+ * EntityID
+ *   The file's id.
+ * FilePath
+ *   Path of the file in the internal file system.  If NULL, an existing file
+ *   entity is simply deleted.
+ * FileSize
+ *   Size of the file in bytes.
+ * FileHash
+ *   A Sha512 Hash of the file.
+ */
+CREATE PROCEDURE setFileProperties (
+    in EntityID INT UNSIGNED,
+    in FilePath TEXT,
+    in FileSize BIGINT UNSIGNED,
+    in FileHash VARCHAR(255)
+)
+BEGIN
+    DECLARE IVersion INT UNSIGNED DEFAULT NULL;
+    IF is_feature_config("ENTITY_VERSIONING", "ENABLED") THEN
+        SELECT max(e._iversion) INTO IVersion
+            FROM entity_version AS e
+            WHERE e.entity_id = EntityID;
+
+        INSERT INTO archive_files (file_id, path, size, hash,
+                _iversion)
+            SELECT file_id, path, size, hash, IVersion AS _iversion
+            FROM files
+            WHERE file_id = EntityID;
+    END IF;
+
+    DELETE FROM files WHERE file_id = EntityID;
+
+    IF FilePath IS NOT NULL THEN
+        INSERT INTO files (file_id, path, size, hash)
+            VALUES (EntityID, FilePath, FileSize, unhex(FileHash));
+    END IF;
+
+END //
+
+
+DROP PROCEDURE IF EXISTS retrieveQueryTemplateDef //
+
+/**
+ * Select a query string from the templates tables.
+ *
+ * Parameters
+ * ----------
+ * EntityID
+ *   The QueryTemplate's id.
+ * Version
+ *   The QueryTemplate's version's id.
+ *
+ * Returns
+ * -------
+ * Tuple (definition). A CQL query string (-fragment) which defines this
+ * QueryTemplate.
+ */
+CREATE PROCEDURE retrieveQueryTemplateDef (
+    in EntityID INT UNSIGNED,
+    in Version VARBINARY(255))
+retrieveQueryTemplateDefBody: BEGIN
+
+    DECLARE IVersion INT UNSIGNED DEFAULT NULL;
+    DECLARE IsHead BOOLEAN DEFAULT TRUE;
+
+    IF is_feature_config("ENTITY_VERSIONING", "ENABLED") THEN
+        -- Are we at the head?
+        IF Version IS NOT NULL THEN
+            SELECT get_head_version(EntityID) = Version INTO IsHead;
+        END IF;
+
+        IF IsHead IS FALSE THEN
+            -- TODO Use get_iversion(EntityID, Version) instead?  Or will that be much slower?
+            SELECT e._iversion INTO IVersion
+                FROM entity_version as e
+                WHERE e.entity_id = EntityID
+                AND e.version = Version;
+
+            IF IVersion IS NULL THEN
+                -- RETURN EARLY - Version does not exist.
+                LEAVE retrieveQueryTemplateDefBody;
+            END IF;
+
+            SELECT definition
+            FROM archive_query_template_def
+            WHERE id = EntityID
+            AND _iversion = IVersion;
+
+            LEAVE retrieveQueryTemplateDefBody;
+        END IF;
+    END IF;
+
+    SELECT definition
+    FROM query_template_def
+    WHERE id = EntityID;
+
+END //
+
+
+delimiter ;
diff --git a/procedures/getFile.sql b/procedures/getFile.sql
deleted file mode 100644
index 43a61dd8dbd236718c7a12ccb4291eacf61323fb..0000000000000000000000000000000000000000
--- a/procedures/getFile.sql
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * ** header v3.0
- * This file is a part of the CaosDB Project.
- *
- * Copyright (C) 2018 Research Group Biomedical Physics,
- * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
- *
- * 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/>.
- *
- * ** end header
- */
-#-- getFile(ID);
-#-- 
-#-- 
-#-- FileID,
-#-- FilePath,
-#-- FileSize,
-#-- FileHash,
-#-- FileDescription,
-#-- FileCreated,
-#-- FileCreator,
-#-- FileGenerator,
-#-- FileOwner,
-#-- FilePermission,
-#-- FileChecksum
-
-Drop Procedure if exists db_2_0.getFile;
-Delimiter //
-Create Procedure db_2_0.getFile (in FileID INT)
-BEGIN 
-
-Select name, description, role into @name, @description, @role from entities where id=FileID LIMIT 1;
-
-IF @role = 'file' Then
-		Select path, hash, size into @FilePath, @FileHash, @FileSize from files where file_id=FileID LIMIT 1;
-		Select timestamp, user_id, user_agent into @FileCreated, @FileCreator, @FileGenerator from history where entity_id=FileID AND event='insertion' LIMIT 1;
-
-Select 
-FileID as FileID,
-@FilePath as FilePath,
-@FileSize as FileSize,
-@FileHash as FileHash,
-@FileDescription as FileDescription,
-@FileCreated as FileCreated,
-@FileCreator as FileCreator,
-@FileGenerator as FileGenerator,
-NULL	as FileOwner,
-NULL as FilePermission,
-NULL as FileChecksum;
-
-END IF;
-
-END;
-//
-delimiter ;
diff --git a/procedures/insertEntity.sql b/procedures/insertEntity.sql
index cfe5700855540c7b8fa67f8bfe0cc98761ac5ec1..50a39979829c381c8e633903bcdef9695ff2a105 100644
--- a/procedures/insertEntity.sql
+++ b/procedures/insertEntity.sql
@@ -5,6 +5,7 @@
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
  * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -39,11 +40,19 @@ Currently one of 'RECORDTYPE', 'RECORD', 'FILE', 'DOMAIN', 'PROPERTY',
 'DATATYPE', 'ROLE', 'QUERYTEMPLATE'
 
 ACL : VARBINARY(65525)
+
+Select
+======
+
+A tuple (EntityID, Version)
 */
 CREATE PROCEDURE db_2_0.insertEntity(in EntityName VARCHAR(255), in EntityDesc TEXT, in EntityRole VARCHAR(255), in ACL VARBINARY(65525))
 BEGIN
     DECLARE NewEntityID INT UNSIGNED DEFAULT NULL;
     DECLARE NewACLID INT UNSIGNED DEFAULT NULL;
+    DECLARE Hash VARBINARY(255) DEFAULT NULL;
+    DECLARE Version VARBINARY(255) DEFAULT NULL;
+    DECLARE Transaction VARBINARY(255) DEFAULT NULL;
 
     -- insert the acl. the new acl id is being written (c-style) into the
     -- variable NewACLID.
@@ -56,15 +65,22 @@ BEGIN
     -- ... and return the generated id
     SET NewEntityID = LAST_INSERT_ID();
 
+    IF is_feature_config("ENTITY_VERSIONING", "ENABLED") THEN
+        -- TODO this is transaction-scoped variable. Is this a good idea?
+        SET Transaction = @SRID;
+        SET Version = SHA1(UUID());
+        CALL insert_single_child_version(NewEntityID, Hash, Version, Null, Transaction);
+    END IF;
+
     -- insert the name of the entity into name_data table
     -- 20 is the (hard-coded) id of the 'name' property.
     IF EntityName IS NOT NULL THEN
-        INSERT INTO name_data 
+        INSERT INTO name_data
             (domain_id, entity_id, property_id, value, status, pidx)
             VALUES (0, NewEntityID, 20, EntityName, "FIX", 0);
     END IF;
 
-    Select NewEntityID as EntityID;
+    SELECT NewEntityID as EntityID, Version as Version;
 
 END;
 //
diff --git a/procedures/insertEntityProperty.sql b/procedures/insertEntityProperty.sql
index 796b8922835502fd64113d20f51d4365f9df2905..d208dbc8347b139eccdddbfa6f980b1f83a6d955 100644
--- a/procedures/insertEntityProperty.sql
+++ b/procedures/insertEntityProperty.sql
@@ -4,6 +4,8 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -23,74 +25,107 @@
 
 DROP PROCEDURE IF EXISTS db_2_0.insertEntityProperty;
 delimiter //
-CREATE PROCEDURE db_2_0.insertEntityProperty(in DomainID INT UNSIGNED, in EntityID INT UNSIGNED, in PropertyID INT UNSIGNED, in Datatable VARCHAR(255), in PropertyValue TEXT, in PropertyUnitSig BIGINT, in PropertyStatus VARCHAR(255), in NameOverride VARCHAR(255), in DescOverride TEXT, in datatypeOverride INT UNSIGNED, in Collection VARCHAR(255), in PropertyIndex INT UNSIGNED)
+CREATE PROCEDURE db_2_0.insertEntityProperty(
+    in DomainID INT UNSIGNED,
+    in EntityID INT UNSIGNED,
+    in PropertyID INT UNSIGNED,
+    in Datatable VARCHAR(255),
+    in PropertyValue TEXT,
+    in PropertyUnitSig BIGINT,
+    in PropertyStatus VARCHAR(255),
+    in NameOverride VARCHAR(255),
+    in DescOverride TEXT,
+    in DatatypeOverride INT UNSIGNED,
+    in Collection VARCHAR(255),
+    in PropertyIndex INT UNSIGNED)
 BEGIN
+    DECLARE ReferenceValueIVersion INT UNSIGNED DEFAULT NULL;
+    DECLARE ReferenceValue INT UNSIGNED DEFAULT NULL;
+    DECLARE AT_PRESENT INTEGER DEFAULT NULL;
 
-	CASE Datatable
-	WHEN 'double_data' THEN
-		INSERT INTO double_data 
-		(domain_id, entity_id, property_id, value, unit_sig, status, pidx) 
-		VALUES 
-		(DomainID, EntityID, PropertyID, PropertyValue, PropertyUnitSig, PropertyStatus, PropertyIndex);
-	WHEN 'integer_data' THEN
-		INSERT INTO integer_data 
-		(domain_id, entity_id, property_id, value, unit_sig, status, pidx) 
-		VALUES 
-		(DomainID, EntityID, PropertyID, PropertyValue, PropertyUnitSig, PropertyStatus, PropertyIndex);
-	WHEN 'datetime_data' THEN
-		INSERT INTO datetime_data 
-		(domain_id, entity_id, property_id, value, value_ns, status, pidx) 
-		VALUES 
-		(DomainID, EntityID, PropertyID, SUBSTRING_INDEX(PropertyValue, 'UTC', 1), IF(SUBSTRING_INDEX(PropertyValue, 'UTC', -1)='',NULL,SUBSTRING_INDEX(PropertyValue, 'UTC', -1)), PropertyStatus, PropertyIndex);
-	WHEN 'reference_data' THEN
-		INSERT INTO reference_data 
-		(domain_id, entity_id, property_id, value, status, pidx) 
-		VALUES 
-		(DomainID, EntityID, PropertyID, PropertyValue, PropertyStatus, PropertyIndex);
-	WHEN 'enum_data' THEN	
-		INSERT INTO enum_data 
-		(domain_id, entity_id, property_id, value, status, pidx) 
-		VALUES 
-		(DomainID, EntityID, PropertyID, PropertyValue, PropertyStatus, PropertyIndex);
-	WHEN 'date_data' THEN	
-		INSERT INTO date_data 
-		(domain_id, entity_id, property_id, value, status, pidx) 
-		VALUES 
-		(DomainID, EntityID, PropertyID, SUBSTRING_INDEX(PropertyValue, '.', 1), PropertyStatus, PropertyIndex);
-	WHEN 'text_data' THEN
-		INSERT INTO text_data 
-		(domain_id, entity_id, property_id, value, status, pidx) 
-		VALUES 
-		(DomainID, EntityID, PropertyID, PropertyValue, PropertyStatus, PropertyIndex);
-	WHEN 'null_data' THEN
-		INSERT INTO null_data
-		(domain_id, entity_id, property_id, status, pidx) 
-		VALUES 
-		(DomainID, EntityID, PropertyID, PropertyStatus, PropertyIndex);
-	WHEN 'name_data' THEN
-		INSERT INTO name_data
-		(domain_id, entity_id, property_id, value, status, pidx) 
-		VALUES 
-		(DomainID, EntityID, PropertyID, PropertyValue, PropertyStatus, PropertyIndex);
+    CASE Datatable
+    WHEN 'double_data' THEN
+        INSERT INTO double_data
+        (domain_id, entity_id, property_id, value, unit_sig, status, pidx)
+        VALUES
+        (DomainID, EntityID, PropertyID, PropertyValue, PropertyUnitSig, PropertyStatus, PropertyIndex);
+    WHEN 'integer_data' THEN
+        INSERT INTO integer_data
+        (domain_id, entity_id, property_id, value, unit_sig, status, pidx)
+        VALUES
+        (DomainID, EntityID, PropertyID, PropertyValue, PropertyUnitSig, PropertyStatus, PropertyIndex);
+    WHEN 'datetime_data' THEN
+        INSERT INTO datetime_data
+        (domain_id, entity_id, property_id, value, value_ns, status, pidx)
+        VALUES
+        (DomainID, EntityID, PropertyID, SUBSTRING_INDEX(PropertyValue, 'UTC', 1), IF(SUBSTRING_INDEX(PropertyValue, 'UTC', -1)='',NULL,SUBSTRING_INDEX(PropertyValue, 'UTC', -1)), PropertyStatus, PropertyIndex);
+    WHEN 'reference_data' THEN
 
-	ELSE	
-		SELECT * FROM table_does_not_exist;
-	END CASE;
+        -- special handling if versioning enabled and specific version of referenced entity is given.
+        SET AT_PRESENT=LOCATE("@", PropertyValue);
+        IF is_feature_config("ENTITY_VERSIONING", "ENABLED") AND AT_PRESENT > 0 THEN
+            SET ReferenceValue = SUBSTRING_INDEX(PropertyValue, '@', 1);
+            SET ReferenceValueIVersion = get_iversion(ReferenceValue,
+                SUBSTRING_INDEX(PropertyValue, '@', -1));
+            -- TODO raise error when @ present but iversion is null
+            IF ReferenceValueIVersion IS NULL THEN
+                SELECT 0 from `ReferenceValueIVersion_WAS_NULL`;
+            END IF;
 
-	IF DatatypeOverride IS NOT NULL THEN
-		call overrideType(DomainID, EntityID, PropertyID, DatatypeOverride);
-		IF Collection IS NOT NULL THEN
-			INSERT INTO collection_type (domain_id, entity_id, property_id, collection) VALUES (DomainID, EntityID, PropertyID, Collection);
-		END IF;
-	END IF;
+        ELSE
+            SET ReferenceValue = PropertyValue;
+        END IF;
 
-	IF NameOverride IS NOT NULL THEN
-		call overrideName(DomainID, EntityID, PropertyID, NameOverride);	
-	END IF;
+        INSERT INTO reference_data
+            (domain_id, entity_id, property_id, value, value_iversion, status,
+                pidx)
+        VALUES
+            (DomainID, EntityID, PropertyID, ReferenceValue,
+                ReferenceValueIVersion, PropertyStatus, PropertyIndex);
+    WHEN 'enum_data' THEN
+        INSERT INTO enum_data
+        (domain_id, entity_id, property_id, value, status, pidx)
+        VALUES
+        (DomainID, EntityID, PropertyID, PropertyValue, PropertyStatus, PropertyIndex);
+    WHEN 'date_data' THEN
+        INSERT INTO date_data
+        (domain_id, entity_id, property_id, value, status, pidx)
+        VALUES
+        (DomainID, EntityID, PropertyID, SUBSTRING_INDEX(PropertyValue, '.', 1), PropertyStatus, PropertyIndex);
+    WHEN 'text_data' THEN
+        INSERT INTO text_data
+        (domain_id, entity_id, property_id, value, status, pidx)
+        VALUES
+        (DomainID, EntityID, PropertyID, PropertyValue, PropertyStatus, PropertyIndex);
+    WHEN 'null_data' THEN
+        INSERT INTO null_data
+        (domain_id, entity_id, property_id, status, pidx)
+        VALUES
+        (DomainID, EntityID, PropertyID, PropertyStatus, PropertyIndex);
+    WHEN 'name_data' THEN
+        INSERT INTO name_data
+        (domain_id, entity_id, property_id, value, status, pidx)
+        VALUES
+        (DomainID, EntityID, PropertyID, PropertyValue, PropertyStatus, PropertyIndex);
 
-	IF DescOverride IS NOT NULL THEN
-		call overrideDesc(DomainID, EntityID, PropertyID, DescOverride);
-	END IF;
+    ELSE
+        SELECT * FROM table_does_not_exist;
+    END CASE;
+
+    IF DatatypeOverride IS NOT NULL THEN
+        call overrideType(DomainID, EntityID, PropertyID, DatatypeOverride);
+        IF Collection IS NOT NULL THEN
+            INSERT INTO collection_type (domain_id, entity_id, property_id, collection) VALUES (DomainID, EntityID, PropertyID, Collection);
+        END IF;
+    END IF;
+
+    IF NameOverride IS NOT NULL THEN
+        call overrideName(DomainID, EntityID, PropertyID, NameOverride);
+    END IF;
+
+    IF DescOverride IS NOT NULL THEN
+        call overrideDesc(DomainID, EntityID, PropertyID, DescOverride);
+    END IF;
 
 END;
 //
diff --git a/procedures/insertIsaCache.sql b/procedures/insertIsaCache.sql
index 68c380865fe21b0a752e6136d2984f2748afb2fe..6b79750982c8b8fa6ba316358da817cdedb9932a 100644
--- a/procedures/insertIsaCache.sql
+++ b/procedures/insertIsaCache.sql
@@ -62,7 +62,7 @@ insert_is_a_proc: BEGIN
            concat(p, ">", i.rpath))      -- Else "p>super.rpath"
             AS rpath
         FROM isa_cache AS i WHERE i.child = p AND i.child != i.parent;  -- Select rows with supertype
-    
+
     -- Propagate to descendants:
     -- for each subtype of c: insert each supertype of p
     INSERT IGNORE INTO isa_cache SELECT
@@ -80,7 +80,7 @@ insert_is_a_proc: BEGIN
         FROM
             isa_cache as l INNER JOIN isa_cache as r
             ON (l.parent = c AND c = r.child AND l.child != l.parent); -- Left: descendants of c, right: ancestors
-	
+
 END;
 //
 
diff --git a/procedures/registerSubdomain.sql b/procedures/registerSubdomain.sql
index 60311f3ab9b2ca17e8e2c91b325f263ab7f19c61..70e8686bb1baf7b816da877c91ba100ae288366a 100644
--- a/procedures/registerSubdomain.sql
+++ b/procedures/registerSubdomain.sql
@@ -31,17 +31,19 @@
 DROP PROCEDURE IF EXISTS db_2_0.registerSubdomain;
 delimiter //
 
-CREATE PROCEDURE db_2_0.registerSubdomain(in Count INT UNSIGNED)
+CREATE PROCEDURE db_2_0.registerSubdomain(in amount INT UNSIGNED)
 BEGIN
     DECLARE ED INTEGER DEFAULT NULL;
 
-Select COUNT(id) INTO ED FROM entities WHERE Role='DOMAIN' AND id!=0;
-WHILE ED < Count DO
-		INSERT INTO entities (description, role, acl) VALUES ("Multipurpose subdomain", 'DOMAIN', 0);
-		SET ED = ED + 1;
-END WHILE;
+    SELECT COUNT(id) INTO ED FROM entities WHERE Role='DOMAIN' AND id!=0;
 
-SELECT id as DomainID FROM entities WHERE Role='DOMAIN' and id!=0;
+    WHILE ED < amount DO
+        INSERT INTO entities (description, role, acl) VALUES
+            (NULL, 'DOMAIN', 0);
+        SET ED = ED + 1;
+    END WHILE;
+
+    SELECT id as DomainID FROM entities WHERE Role='DOMAIN' and id!=0;
 
 END;
 //
diff --git a/procedures/retrieveEntity.sql b/procedures/retrieveEntity.sql
index 0fbab94715a1903282ae18875c2184857357c23a..f74e6233af941ef3762647448e4285c4a5d26366 100644
--- a/procedures/retrieveEntity.sql
+++ b/procedures/retrieveEntity.sql
@@ -4,6 +4,8 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -27,31 +29,168 @@ delimiter //
 
 drop procedure if exists db_2_0.retrieveEntity //
 
-create procedure db_2_0.retrieveEntity(in EntityID INT UNSIGNED)
-BEGIN
-	DECLARE FilePath VARCHAR(255) DEFAULT NULL;
-	DECLARE FileSize VARCHAR(255) DEFAULT NULL;
-	DECLARE FileHash VARCHAR(255) DEFAULT NULL;
-	DECLARE DatatypeID INT UNSIGNED DEFAULT NULL;
+/**
+ * Select the content of an entity.
+ *
+ * By default the head is selected, but a specific version may be given.
+ *
+ * Parameters
+ * ----------
+ * EntityID
+ *   The entity's id.
+ * Version
+ *   The version id.  In this procedure only, the version may also be given as
+ *   `HEAD` for the latest version or as `HEAD~n`, which retrieves the n-th
+ *   ancestor of `HEAD`.
+ *
+ */
+create procedure db_2_0.retrieveEntity(
+    in EntityID INT UNSIGNED,
+    in Version VARBINARY(255))
+retrieveEntityBody: BEGIN
+    DECLARE FilePath VARCHAR(255) DEFAULT NULL;
+    DECLARE FileSize VARCHAR(255) DEFAULT NULL;
+    DECLARE FileHash VARCHAR(255) DEFAULT NULL;
+    DECLARE DatatypeID INT UNSIGNED DEFAULT NULL;
     DECLARE CollectionName VARCHAR(255) DEFAULT NULL;
-	
-	Select path, size, hex(hash) into FilePath, FileSize, FileHash from files where file_id = EntityID LIMIT 1;
-	Select datatype into DatatypeID from data_type where domain_id=0 and entity_id=0 and property_id=EntityID LIMIT 1;
-
-	SELECT collection into CollectionName from collection_type where domain_id=0 and entity_id=0 and property_id=EntityID LIMIT 1;
-
-	Select 
-		(SELECT value FROM name_data WHERE domain_id = 0 AND entity_ID = DatatypeID AND property_id = 20 LIMIT 1) as Datatype,
-		CollectionName as Collection,
-		EntityID as EntityID, 
-		(SELECT value FROM name_data WHERE domain_id = 0 AND entity_ID = EntityID AND property_id = 20 LIMIT 1) as EntityName,
-		e.description as EntityDesc, 
-		e.role as EntityRole, 
-		FileSize as FileSize, 
-		FilePath as FilePath, 
-		FileHash as FileHash,
-		(SELECT acl FROM entity_acl as a WHERE a.id = e.acl LIMIT 1) as ACL
-	from entities e where id = EntityID LIMIT 1;
+    DECLARE VersionSeconds BIGINT UNSIGNED DEFAULT NULL;
+    DECLARE VersionNanos INT(10) UNSIGNED DEFAULT NULL;
+    DECLARE IsHead BOOLEAN DEFAULT TRUE;
+    DECLARE IVersion INT UNSIGNED DEFAULT NULL;
+
+
+    IF is_feature_config("ENTITY_VERSIONING", "ENABLED") THEN
+        -- Find out head-ness and version
+        IF Version IS NULL OR UPPER(Version) = "HEAD" THEN
+            SET Version = get_head_version(EntityID);
+        ELSEIF UPPER(LEFT(Version, 5)) = "HEAD~" THEN
+            SET IsHead = FALSE;
+            SET Version = get_head_relative(EntityID, SUBSTR(Version, 6));
+        ELSE
+            SELECT get_head_version(EntityID) = Version INTO IsHead;
+        END IF;
+
+        SELECT t.seconds, t.nanos
+            INTO VersionSeconds, VersionNanos
+            FROM entity_version AS e
+            INNER JOIN transactions AS t
+            ON ( e.srid = t.srid )
+            WHERE e.entity_id = EntityID
+            AND e.version = Version;
+
+        IF IsHead IS FALSE THEN
+            SET IVersion=get_iversion(EntityID, Version);
+
+            IF IVersion IS NULL THEN
+                -- RETURN EARLY - Version does not exist.
+                SELECT 0 WHERE 0 = 1;
+                LEAVE retrieveEntityBody;
+            END IF;
+
+            SELECT path, size, HEX(hash)
+                INTO FilePath, FileSize, FileHash
+                FROM archive_files
+                WHERE file_id = EntityID
+                AND _iversion = IVersion
+                LIMIT 1;
+
+            SELECT datatype
+                INTO DatatypeID
+                FROM archive_data_type
+                WHERE domain_id = 0
+                AND entity_id = 0
+                AND property_id = EntityID
+                AND _iversion = IVersion
+                LIMIT 1;
+
+            SELECT collection
+                INTO CollectionName
+                FROM archive_collection_type
+                WHERE domain_id = 0
+                AND entity_id = 0
+                AND property_id = EntityID
+                AND _iversion = IVersion
+                LIMIT 1;
+
+            -- Final select before returning
+            SELECT
+                ( SELECT value FROM
+                    ( SELECT value FROM name_data
+                        WHERE domain_id = 0
+                        AND entity_ID = DatatypeID
+                        AND property_id = 20
+                        UNION SELECT DatatypeID AS value
+                    ) AS tmp LIMIT 1 ) AS Datatype,
+                CollectionName AS Collection,
+                EntityID AS EntityID,
+                ( SELECT value FROM archive_name_data
+                    WHERE domain_id = 0
+                    AND entity_ID = EntityID
+                    AND property_id = 20
+                    AND _iversion = IVersion
+                    -- LIMIT 1 -- TODO Remove this line if all tests pass.
+                    ) AS EntityName,
+                e.description AS EntityDesc,
+                e.role AS EntityRole,
+                FileSize AS FileSize,
+                FilePath AS FilePath,
+                FileHash AS FileHash,
+                (SELECT acl FROM entity_acl AS a WHERE a.id = e.acl) AS ACL,
+                Version AS Version,
+                VersionSeconds AS VersionSeconds,
+                VersionNanos AS VersionNanos
+            FROM archive_entities AS e
+            WHERE e.id = EntityID
+            AND e._iversion = IVersion
+            LIMIT 1;
+
+            -- RETURN EARLY
+            LEAVE retrieveEntityBody;
+
+        END IF;
+    END IF;
+
+    SELECT path, size, hex(hash)
+        INTO FilePath, FileSize, FileHash
+        FROM files
+        WHERE file_id = EntityID
+        LIMIT 1;
+
+    SELECT datatype INTO DatatypeID
+        FROM data_type
+        WHERE domain_id=0
+        AND entity_id=0
+        AND property_id=EntityID
+        LIMIT 1;
+
+    SELECT collection INTO CollectionName
+        FROM collection_type
+        WHERE domain_id=0
+        AND entity_id=0
+        AND property_id=EntityID
+        LIMIT 1;
+
+    SELECT
+        ( SELECT value FROM name_data
+            WHERE domain_id = 0
+            AND entity_ID = DatatypeID
+            AND property_id = 20 LIMIT 1 ) AS Datatype,
+        CollectionName AS Collection,
+        EntityID AS EntityID,
+        ( SELECT value FROM name_data
+            WHERE domain_id = 0
+            AND entity_ID = EntityID
+            AND property_id = 20 LIMIT 1) AS EntityName,
+        e.description AS EntityDesc,
+        e.role AS EntityRole,
+        FileSize AS FileSize,
+        FilePath AS FilePath,
+        FileHash AS FileHash,
+        (SELECT acl FROM entity_acl AS a WHERE a.id = e.acl) AS ACL,
+        Version AS Version,
+        VersionSeconds AS VersionSeconds,
+        VersionNanos AS VersionNanos
+    FROM entities e WHERE id = EntityID LIMIT 1;
 END;
 //
 
diff --git a/procedures/retrieveEntityOverrides.sql b/procedures/retrieveEntityOverrides.sql
index 3c7421ccb639e9f709706fb1cf2fd28bb2bb9f01..fa43d6b3e216cdc25798261a8753daa656cf8618 100644
--- a/procedures/retrieveEntityOverrides.sql
+++ b/procedures/retrieveEntityOverrides.sql
@@ -4,6 +4,8 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2020 Indiscale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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
@@ -24,14 +26,157 @@
 DROP PROCEDURE IF EXISTS db_2_0.retrieveOverrides;
 delimiter //
 
-CREATE PROCEDURE db_2_0.retrieveOverrides(in DomainID INT UNSIGNED, in EntityID INT UNSIGNED)
-BEGIN
+CREATE PROCEDURE db_2_0.retrieveOverrides(
+    in DomainID INT UNSIGNED,
+    in EntityID INT UNSIGNED,
+    in Version VARBINARY(255))
+retrieveOverridesBody: BEGIN
+
+    DECLARE IVersion INT UNSIGNED DEFAULT NULL;
+    DECLARE IsHead BOOLEAN DEFAULT TRUE;
+
+    IF is_feature_config("ENTITY_VERSIONING", "ENABLED") THEN
+        IF Version IS NOT NULL THEN
+            IF DomainID = 0 THEN
+                SELECT get_head_version(EntityID) = Version INTO IsHead;
+            ELSE
+                SELECT get_head_version(DomainID) = Version INTO IsHead;
+            END IF;
+        END IF;
+
+        IF IsHead IS FALSE THEN
+            SELECT e._iversion INTO IVersion
+                FROM entity_version as e
+                WHERE ((e.entity_id = EntityID AND DomainID = 0)
+                    OR (e.entity_id = DomainID))
+                AND e.version = Version;
+
+            IF IVersion IS NULL THEN
+                -- RETURN EARLY - Version does not exist.
+                LEAVE retrieveOverridesBody;
+            END IF;
+
+            -- name
+            SELECT
+                NULL AS collection_override,
+                name AS name_override,
+                NULL AS desc_override,
+                NULL AS type_override,
+                entity_id,
+                property_id
+            FROM archive_name_overrides
+            WHERE domain_id = DomainID
+            AND entity_id = EntityID
+            AND _iversion = IVersion
+
+            UNION ALL
+
+            -- description
+            SELECT
+                NULL AS collection_override,
+                NULL AS name_override,
+                description AS desc_override,
+                NULL AS type_override,
+                entity_id,
+                property_id
+            FROM archive_desc_overrides
+            WHERE domain_id = DomainID
+            AND entity_id = EntityID
+            AND _iversion = IVersion
+
+            UNION ALL
+
+            -- data type
+            SELECT
+                NULL AS collection_override,
+                NULL AS name_override,
+                NULL AS desc_override,
+                ( SELECT value FROM
+                    ( SELECT value FROM name_data
+                        WHERE domain_id = 0
+                        AND entity_ID = datatypeID
+                        AND property_id = 20
+                        UNION SELECT datatype AS value
+                    ) AS tmp LIMIT 1 ) AS type_override,
+                entity_id,
+                property_id
+            FROM archive_data_type
+            WHERE domain_id = DomainID
+            AND entity_id = EntityID
+            AND _iversion = IVersion
+
+            UNION ALL
+
+            -- collection
+            SELECT
+                collection AS collection_override,
+                NULL AS name_override,
+                NULL AS desc_override,
+                NULL AS type_override,
+                entity_id,
+                property_id
+            FROM archive_collection_type
+            WHERE domain_id = DomainID
+            AND entity_id = EntityID
+            AND _iversion = IVersion;
+
+            LEAVE retrieveOverridesBody;
+        END IF;
+    END IF;
+
+    SELECT
+        NULL AS collection_override,
+        name AS name_override,
+        NULL AS desc_override,
+        NULL AS type_override,
+        entity_id,
+        property_id
+    FROM name_overrides
+    WHERE domain_id = DomainID
+    AND entity_id = EntityID
+
+    UNION ALL
+
+    SELECT
+        NULL AS collection_override,
+        NULL AS name_override,
+        description AS desc_override,
+        NULL AS type_override,
+        entity_id,
+        property_id
+    FROM desc_overrides
+    WHERE domain_id = DomainID
+    AND entity_id = EntityID
+
+    UNION ALL
+
+    SELECT
+        NULL AS collection_override,
+        NULL AS name_override,
+        NULL AS desc_override,
+        ( SELECT value FROM name_data
+            WHERE domain_id = 0
+            AND entity_ID = datatype
+            AND property_id = 20 LIMIT 1 ) AS type_override,
+        entity_id,
+        property_id
+    FROM data_type
+    WHERE domain_id = DomainID
+    AND entity_id = EntityID
+
+    UNION ALL
+
+    SELECT
+        collection AS collection_override,
+        NULL AS name_override,
+        NULL AS desc_override,
+        NULL AS type_override,
+        entity_id,
+        property_id
+    FROM collection_type
+    WHERE domain_id = DomainID
+    AND entity_id = EntityID;
 
-	SELECT NULL as collection_override, name as name_override, NULL as desc_override, NULL as type_override, entity_id, property_id from name_overrides where domain_id=DomainID and entity_id=EntityID UNION ALL
-	SELECT NULL as collection_override, NULL as name_override, description as desc_override, NULL as type_override, entity_id, property_id from desc_overrides where domain_id=DomainID and entity_id=EntityID UNION ALL
-	SELECT NULL as collection_override, NULL as name_override, NULL as desc_override, (SELECT value FROM name_data WHERE domain_id = 0 AND entity_ID = datatype AND property_id = 20) as type_override, entity_id, property_id from data_type where domain_id=DomainID and entity_id=EntityID UNION ALL
-	SELECT collection as collection_override, NULL as name_override, NULL as desc_override, NULL as type_override, entity_id, property_id from collection_type where domain_id=DomainID and entity_id=EntityID;
-	
 
 END;
 //
diff --git a/procedures/retrieveEntityParents.sql b/procedures/retrieveEntityParents.sql
index 27d8719e8b680ec48e85e769315a192e82014a0e..16466fc991693766c1bfded1acdeec453e2cd655 100644
--- a/procedures/retrieveEntityParents.sql
+++ b/procedures/retrieveEntityParents.sql
@@ -5,6 +5,7 @@
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
  * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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
@@ -51,13 +52,62 @@ ParentRole :
 ACL :
   Access control list something
 */
-CREATE PROCEDURE db_2_0.retrieveEntityParents(in EntityID INT UNSIGNED)
-BEGIN
+CREATE PROCEDURE db_2_0.retrieveEntityParents(
+    in EntityID INT UNSIGNED,
+    in Version VARBINARY(255))
+retrieveEntityParentsBody: BEGIN
 
-SELECT parent AS ParentID, (SELECT value FROM name_data WHERE domain_id = 0 AND entity_ID = ParentID AND property_id = 20) AS ParentName, description AS ParentDescription, role AS ParentRole, (SELECT acl from entity_acl as a WHERE a.id=e.acl) AS ACL FROM isa_cache AS i JOIN entities AS e ON (i.parent=e.id AND i.child=EntityID and i.rpath=EntityID); 
+    DECLARE IVersion INT UNSIGNED DEFAULT NULL;
+    DECLARE IsHead BOOLEAN DEFAULT TRUE;
 
+    IF is_feature_config("ENTITY_VERSIONING", "ENABLED") THEN
+        IF Version IS NOT NULL THEN
+            SELECT get_head_version(EntityID) = Version INTO IsHead;
+        END IF;
 
+        IF IsHead IS FALSE THEN
+            SELECT e._iversion INTO IVersion
+                FROM entity_version as e
+                WHERE e.entity_id = EntityID
+                AND e.version = Version;
 
+            IF IVersion IS NULL THEN
+                -- RETURN EARLY - Version does not exist.
+                LEAVE retrieveEntityParentsBody;
+            END IF;
+
+            SELECT
+                i.parent AS ParentID,
+                ( SELECT value FROM archive_name_data
+                    WHERE domain_id = 0
+                    AND entity_id = ParentID
+                    AND property_id = 20
+                    AND _iversion = IVersion) AS ParentName,
+                e.description AS ParentDescription,
+                e.role AS ParentRole,
+                (SELECT acl FROM entity_acl AS a WHERE a.id = e.acl) AS ACL
+                FROM archive_isa AS i JOIN entities AS e
+                    ON (i.parent = e.id)
+                WHERE i.child = EntityID
+                AND i.child_iversion = IVersion;
+
+            LEAVE retrieveEntityParentsBody;
+        END IF;
+    END IF;
+
+    SELECT
+        i.parent AS ParentID,
+        ( SELECT value FROM name_data
+            WHERE domain_id = 0
+            AND entity_id = ParentID
+            AND property_id = 20 ) AS ParentName,
+        e.description AS ParentDescription,
+        e.role AS ParentRole,
+        (SELECT acl FROM entity_acl AS a WHERE a.id = e.acl) AS ACL
+        FROM isa_cache AS i JOIN entities AS e
+            ON (i.parent = e.id)
+        WHERE i.child = EntityID
+        AND i.rpath = EntityID;
 END
 //
 
diff --git a/procedures/retrieveEntityProperties.sql b/procedures/retrieveEntityProperties.sql
index be778745b215f896fad4e3bca0ad932a965406de..3d8eeffad39544cba57c68a820d2479aa58a20b1 100644
--- a/procedures/retrieveEntityProperties.sql
+++ b/procedures/retrieveEntityProperties.sql
@@ -4,6 +4,8 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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
@@ -26,61 +28,270 @@ delimiter //
 
 drop procedure if exists db_2_0.retrieveEntityProperties //
 
-create procedure db_2_0.retrieveEntityProperties(in DomainID INT UNSIGNED, in EntityID INT UNSIGNED)
-BEGIN
+create procedure db_2_0.retrieveEntityProperties(
+    in DomainID INT UNSIGNED,
+    in EntityID INT UNSIGNED,
+    in Version VARBINARY(255))
+retrieveEntityPropertiesBody: BEGIN
 
-		#-- double properties
-		Select 
-		property_id as PropertyID, value as PropertyValue, status as PropertyStatus, pidx as PropertyIndex from double_data where domain_id = DomainID and entity_id = EntityID
+    DECLARE IVersion INT UNSIGNED DEFAULT NULL;
+    DECLARE IsHead BOOLEAN DEFAULT TRUE;
 
-		UNION ALL
-		
-		#-- integer properties
-		Select 
-		property_id as PropertyID, value as PropertyValue, status as PropertyStatus, pidx as PropertyIndex from integer_data where domain_id = DomainID and entity_id = EntityID
+    IF is_feature_config("ENTITY_VERSIONING", "ENABLED") THEN
+        IF Version IS NOT NULL THEN
+            IF DomainID = 0 THEN
+                SELECT get_head_version(EntityID) = Version INTO IsHead;
+            ELSE
+                SELECT get_head_version(DomainID) = Version INTO IsHead;
+            END IF;
 
-		UNION ALL
+        END IF;
 
-		#-- date properties
-		Select 
-		property_id as PropertyID, CONCAT(value, '.NULL.NULL') as PropertyValue, status as PropertyStatus, pidx as PropertyIndex from date_data where domain_id = DomainID and entity_id = EntityID
+        IF IsHead IS FALSE THEN
+            SELECT e._iversion INTO IVersion
+                FROM entity_version as e
+                WHERE ((e.entity_id = EntityID AND DomainID = 0)
+                    OR (e.entity_id = DomainID))
+                AND e.version = Version;
 
-		UNION ALL
+            IF IVersion IS NULL THEN
+                -- RETURN EARLY - Version does not exist.
+                LEAVE retrieveEntityPropertiesBody;
+            END IF;
 
-		#-- datetime properties
-		Select 
-		property_id as PropertyID, CONCAT(value, 'UTC', IF(value_ns IS NULL, '', value_ns)) as PropertyValue, status as PropertyStatus, pidx as PropertyIndex from datetime_data where domain_id = DomainID and entity_id = EntityID
+            #-- double properties
+            SELECT
+                property_id AS PropertyID,
+                value AS PropertyValue,
+                status AS PropertyStatus,
+                pidx AS PropertyIndex
+            FROM archive_double_data
+            WHERE domain_id = DomainID
+            AND entity_id = EntityID
+            AND _iversion = IVersion
 
-		UNION ALL
+            UNION ALL
 
-		#-- text properties
-		Select 
-		property_id as PropertyID, value as PropertyValue, status as PropertyStatus, pidx as PropertyIndex from text_data where domain_id = DomainID and entity_id = EntityID
+            #-- integer properties
+            SELECT
+                property_id AS PropertyID,
+                value AS PropertyValue,
+                status AS PropertyStatus,
+                pidx AS PropertyIndex
+            FROM archive_integer_data
+            WHERE domain_id = DomainID
+            AND entity_id = EntityID
+            AND _iversion = IVersion
 
-		UNION ALL
+            UNION ALL
 
-		#-- enum properties
-		Select 
-		property_id as PropertyID, value as PropertyValue, status as PropertyStatus, pidx as PropertyIndex from enum_data where domain_id = DomainID and entity_id = EntityID
+            #-- date properties
+            SELECT
+                property_id AS PropertyID,
+                CONCAT(value, '.NULL.NULL') AS PropertyValue,
+                status AS PropertyStatus,
+                pidx AS PropertyIndex
+            FROM archive_date_data
+            WHERE domain_id = DomainID
+            AND entity_id = EntityID
+            AND _iversion = IVersion
 
-		UNION ALL
+            UNION ALL
 
-		#-- reference properties
-		Select 
-		property_id as PropertyID, value as PropertyValue, status as PropertyStatus, pidx as PropertyIndex from reference_data where domain_id = DomainID and entity_id = EntityID
+            #-- datetime properties
+            SELECT
+                property_id AS PropertyID,
+                CONCAT(value, 'UTC', IF(value_ns IS NULL, '', value_ns))
+                    AS PropertyValue,
+                status AS PropertyStatus,
+                pidx AS PropertyIndex
+            FROM archive_datetime_data
+            WHERE domain_id = DomainID
+            AND entity_id = EntityID
+            AND _iversion = IVersion
 
-		UNION ALL
+            UNION ALL
 
-		#-- null properties
-		Select
-		property_id as PropertyID, NULL AS PropertyValue, status as PropertyStatus, pidx as PropertyIndex from null_data WHERE domain_id = DomainID and entity_id = EntityID
-		
-		UNION ALL
+            #-- text properties
+            SELECT
+                property_id AS PropertyID,
+                value AS PropertyValue,
+                status AS PropertyStatus,
+                pidx AS PropertyIndex
+            FROM archive_text_data
+            WHERE domain_id = DomainID
+            AND entity_id = EntityID
+            AND _iversion = IVersion
 
-		#-- name properties
-		Select 
-		property_id as PropertyID, value as PropertyValue, status as PropertyStatus, pidx as PropertyIndex from name_data where domain_id = DomainID and entity_id = EntityID and property_id!=20;
+            UNION ALL
 
+            #-- enum properties
+            SELECT
+                property_id AS PropertyID,
+                value AS PropertyValue,
+                status AS PropertyStatus,
+                pidx AS PropertyIndex
+            FROM archive_enum_data
+            WHERE domain_id = DomainID
+            AND entity_id = EntityID
+            AND _iversion = IVersion
+
+            UNION ALL
+
+            #-- reference properties
+            SELECT
+                property_id AS PropertyID,
+                IF(value_iversion IS NULL, value,
+                    -- make it "value@version" if necessary
+                    CONCAT(value, "@", _get_version(value, value_iversion)))
+                    AS PropertyValue,
+                status AS PropertyStatus,
+                pidx AS PropertyIndex
+            FROM archive_reference_data
+            WHERE domain_id = DomainID
+            AND entity_id = EntityID
+            AND _iversion = IVersion
+
+            UNION ALL
+
+            #-- null properties
+            SELECT
+                property_id AS PropertyID,
+                NULL AS PropertyValue,
+                status AS PropertyStatus,
+                pidx AS PropertyIndex
+            FROM archive_null_data
+            WHERE domain_id = DomainID
+            AND entity_id = EntityID
+            AND _iversion = IVersion
+
+            UNION ALL
+
+            #-- name properties
+            SELECT
+                property_id AS PropertyID,
+                value AS PropertyValue,
+                status AS PropertyStatus,
+                pidx AS PropertyIndex
+            FROM archive_name_data
+            WHERE domain_id = DomainID
+            AND entity_id = EntityID
+            AND property_id != 20
+            AND _iversion = IVersion;
+
+            LEAVE retrieveEntityPropertiesBody;
+        END IF;
+    END IF;
+
+    #-- double properties
+    SELECT
+        property_id AS PropertyID,
+        value AS PropertyValue,
+        status AS PropertyStatus,
+        pidx AS PropertyIndex
+    FROM double_data
+    WHERE domain_id = DomainID
+    AND entity_id = EntityID
+
+    UNION ALL
+
+    #-- integer properties
+    SELECT
+        property_id AS PropertyID,
+        value AS PropertyValue,
+        status AS PropertyStatus,
+        pidx AS PropertyIndex
+    FROM integer_data
+    WHERE domain_id = DomainID
+    AND entity_id = EntityID
+
+    UNION ALL
+
+    #-- date properties
+    SELECT
+        property_id AS PropertyID,
+        CONCAT(value, '.NULL.NULL') AS PropertyValue,
+        status AS PropertyStatus,
+        pidx AS PropertyIndex
+    FROM date_data
+    WHERE domain_id = DomainID
+    AND entity_id = EntityID
+
+    UNION ALL
+
+    #-- datetime properties
+    SELECT
+        property_id AS PropertyID,
+        CONCAT(value, 'UTC', IF(value_ns IS NULL, '', value_ns))
+            AS PropertyValue,
+        status AS PropertyStatus,
+        pidx AS PropertyIndex
+    FROM datetime_data
+    WHERE domain_id = DomainID
+    AND entity_id = EntityID
+
+    UNION ALL
+
+    #-- text properties
+    SELECT
+        property_id AS PropertyID,
+        value AS PropertyValue,
+        status AS PropertyStatus,
+        pidx AS PropertyIndex
+    FROM text_data
+    WHERE domain_id = DomainID
+    AND entity_id = EntityID
+
+    UNION ALL
+
+    #-- enum properties
+    SELECT
+        property_id AS PropertyID,
+        value AS PropertyValue,
+        status AS PropertyStatus,
+        pidx AS PropertyIndex
+    FROM enum_data
+    WHERE domain_id = DomainID
+    AND entity_id = EntityID
+
+    UNION ALL
+
+    #-- reference properties
+    SELECT
+        property_id AS PropertyID,
+        IF(value_iversion IS NULL, value,
+            CONCAT(value, "@", _get_version(value, value_iversion)))
+            AS PropertyValue,
+        status AS PropertyStatus,
+        pidx AS PropertyIndex
+    FROM reference_data
+    WHERE domain_id = DomainID
+    AND entity_id = EntityID
+
+    UNION ALL
+
+    #-- null properties
+    SELECT
+        property_id AS PropertyID,
+        NULL AS PropertyValue,
+        status AS PropertyStatus,
+        pidx AS PropertyIndex
+    FROM null_data
+    WHERE domain_id = DomainID
+    AND entity_id = EntityID
+
+    UNION ALL
+
+    #-- name properties
+    SELECT
+        property_id AS PropertyID,
+        value AS PropertyValue,
+        status AS PropertyStatus,
+        pidx AS PropertyIndex
+    FROM name_data
+    WHERE domain_id = DomainID
+    AND entity_id = EntityID
+    AND property_id != 20;
 
 
 END;
diff --git a/procedures/updateEntity.sql b/procedures/updateEntity.sql
index 76c4bd25c8268896c789fbf37d847098f96476ec..226208e967f7d3063ad8fb7e922133ac5c28b8ee 100644
--- a/procedures/updateEntity.sql
+++ b/procedures/updateEntity.sql
@@ -4,6 +4,8 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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
@@ -21,23 +23,79 @@
  * ** end header
  */
 
+DROP PROCEDURE IF EXISTS db_2_0.updateEntity;
+delimiter //
 
+/*
+ * Update an entity (that is, its metadata like name, description, ...).
+ *
+ * At the moment, the version ID is generated internally.
+ *
+ * Selects the new version identifier for the entity.
+ */
+CREATE PROCEDURE db_2_0.updateEntity(
+    in EntityID INT UNSIGNED,
+    in EntityName VARCHAR(255),
+    in EntityDescription TEXT,
+    in EntityRole VARCHAR(255),
+    in Datatype VARCHAR(255),
+    in Collection VARCHAR(255),
+    in ACL VARBINARY(65525))
+BEGIN
+    DECLARE ACLID INT UNSIGNED DEFAULT NULL;
+    DECLARE Hash VARBINARY(255) DEFAULT NULL;
+    DECLARE Version VARBINARY(255) DEFAULT SHA1(UUID());
+    DECLARE ParentVersion VARBINARY(255) DEFAULT NULL;
+    DECLARE Transaction VARBINARY(255) DEFAULT NULL;
+    DECLARE OldIVersion INT UNSIGNED DEFAULT NULL;
 
+    call entityACL(ACLID, ACL);
 
+    IF is_feature_config("ENTITY_VERSIONING", "ENABLED") THEN
+        SELECT max(_iversion) INTO OldIVersion
+            FROM entity_version
+            WHERE entity_id = EntityID;
 
+        -- move old data to archives
+        INSERT INTO archive_entities (id, description, role,
+                acl, _iversion)
+            SELECT e.id, e.description, e.role, e.acl, OldIVersion
+            FROM entities AS e
+            WHERE e.id = EntityID;
 
+        INSERT INTO archive_data_type (domain_id, entity_id, property_id,
+                datatype, _iversion)
+            SELECT e.domain_id, e.entity_id, e.property_id, e.datatype,
+                OldIVersion
+            FROM data_type AS e
+            WHERE e.domain_id = 0
+            AND e.entity_id = 0
+            AND e.property_id = EntityID;
 
+        INSERT INTO archive_collection_type (domain_id, entity_id, property_id,
+                collection, _iversion)
+            SELECT e.domain_id, e.entity_id, e.property_id, e.collection,
+                OldIVersion
+            FROM collection_type as e
+            WHERE e.domain_id = 0
+            AND e.entity_id = 0
+            AND e.property_id = EntityID;
 
-DROP PROCEDURE IF EXISTS db_2_0.updateEntity;
-delimiter //
 
-CREATE PROCEDURE db_2_0.updateEntity(in EntityID INT UNSIGNED, in EntityName VARCHAR(255), in EntityDescription TEXT, in EntityRole VARCHAR(255), in Datatype VARCHAR(255), in Collection VARCHAR(255), in ACL VARBINARY(65525))
-BEGIN 
-    DECLARE ACLID INT UNSIGNED DEFAULT NULL;
-    call entityACL(ACLID, ACL);
+        SET Transaction = @SRID;
+        SELECT e.version INTO ParentVersion
+            FROM entity_version as e
+            WHERE e.entity_id = EntityID
+            AND e._iversion = OldIVersion;
+        CALL insert_single_child_version(
+            EntityID, Hash, Version,
+            ParentVersion, Transaction);
+    END IF;
 
     UPDATE entities e
-        SET e.description = EntityDescription, e.role=EntityRole, e.acl = ACLID
+        SET e.description = EntityDescription,
+            e.role=EntityRole,
+            e.acl = ACLID
         WHERE e.id = EntityID;
 
     -- clean up primary name, because updateEntity might be called without a
@@ -50,16 +108,27 @@ BEGIN
             VALUES (0, EntityID, 20, EntityName, "FIX", 0);
     END IF;
 
-    DELETE from data_type where domain_id=0 AND entity_id=0 AND property_id=EntityID;
-    DELETE from collection_type where domain_id=0 AND entity_id=0 AND property_id=EntityID;
+    DELETE FROM data_type
+        WHERE domain_id=0 AND entity_id=0 AND property_id=EntityID;
+
+    DELETE FROM collection_type
+        WHERE domain_id=0 AND entity_id=0 AND property_id=EntityID;
 
     IF Datatype IS NOT NULL THEN
-        INSERT INTO data_type (domain_id, entity_id, property_id, datatype) SELECT 0, 0, EntityID, ( SELECT entity_id FROM name_data WHERE domain_id = 0 AND property_id = 20 AND value = Datatype LIMIT 1 );
+        INSERT INTO data_type (domain_id, entity_id, property_id, datatype)
+            SELECT 0, 0, EntityID,
+                ( SELECT entity_id FROM name_data WHERE domain_id = 0
+                    AND property_id = 20 AND value = Datatype LIMIT 1 );
+
         IF Collection IS NOT NULL THEN
-            INSERT INTO collection_type (domain_id, entity_id, property_id, collection) SELECT 0, 0, EntityID, Collection;
+            INSERT INTO collection_type (domain_id, entity_id, property_id,
+                    collection)
+                SELECT 0, 0, EntityID, Collection;
         END IF;
     END IF;
 
+    Select Version as Version;
+
 END;
 //
 delimiter ;
diff --git a/tests/docker_env/.config b/tests/docker_env/.config
new file mode 100644
index 0000000000000000000000000000000000000000..393b835526d3082442f24ae6f68355fa00a93b6f
--- /dev/null
+++ b/tests/docker_env/.config
@@ -0,0 +1,11 @@
+# Config for tests with a Docker SQL server
+
+# The host of the MySQL server.
+MYSQL_HOST=127.0.0.1
+# The port number of the MySQL server.
+MYSQL_PORT=3306
+# The user for the installation. Note: This is not the user
+# which will then be used by the CaosDB Server.
+MYSQL_USER=root
+MYSQL_USER_PASSWORD=pass-for-test
+DATABASE_USER_HOST_LIST=127.0.0.1,%,
\ No newline at end of file
diff --git a/tests/docker_env/config.defaults b/tests/docker_env/config.defaults
new file mode 120000
index 0000000000000000000000000000000000000000..149ebfcfa2af9f924d46a202ad4d5af6af0c9834
--- /dev/null
+++ b/tests/docker_env/config.defaults
@@ -0,0 +1 @@
+../../config.defaults
\ No newline at end of file
diff --git a/tests/test_0_fix_unversioned.sql b/tests/test_0_fix_unversioned.sql
new file mode 120000
index 0000000000000000000000000000000000000000..169bdb1e4438c8ec0d3f8a6a3ad7c6081cd76ce2
--- /dev/null
+++ b/tests/test_0_fix_unversioned.sql
@@ -0,0 +1 @@
+../patches/patch20200710-3.0.0-rc2/fix_unversioned.sql
\ No newline at end of file
diff --git a/tests/test_0_setup.sql b/tests/test_0_setup.sql
new file mode 100644
index 0000000000000000000000000000000000000000..40688591f840ed62ee689cae2787dc6727f8b367
--- /dev/null
+++ b/tests/test_0_setup.sql
@@ -0,0 +1,46 @@
+/**
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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/>.
+ *
+ * ** end header
+ */
+
+/**
+ * Execute an SQL statement and pass if the statement throws an SQLEXCEPTION.
+ * Otherwise, fail with the given msg.
+ */
+DROP PROCEDURE IF EXISTS tap._assert_throws;
+delimiter //
+CREATE PROCEDURE tap._assert_throws(in stmt TEXT, in msg TEXT)
+BEGIN
+    DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN
+        SELECT tap.pass(msg);
+    END;
+    SET @stmt = stmt;
+    PREPARE _stmt from @stmt;
+    EXECUTE _stmt;
+
+    SELECT tap.fail(msg);
+END //
+
+delimiter ;
+
+
+-- test helper
+-- CALL tap._assert_throws("SELECT * FROM non-existing", "should throw an error");
diff --git a/tests/test_autotap.sql b/tests/test_autotap.sql
index 0e550c221552515b6b73170baf53fcaca307aa0a..26af8b87fbf61e98ef34f0a2a6b37efb296452c1 100644
--- a/tests/test_autotap.sql
+++ b/tests/test_autotap.sql
@@ -33,8 +33,6 @@ SELECT tap.has_schema('_caosdb_schema_unit_tests','');
 -- TABLES 
 -- ***************************************************************
 
-SELECT tap.tables_are('_caosdb_schema_unit_tests','`collection_type`,`data_type`,`date_data`,`datetime_data`,`desc_overrides`,`double_data`,`entities`,`entity_acl`,`enum_data`,`files`,`integer_data`,`isa_cache`,`logging`,`name_data`,`name_overrides`,`null_data`,`passwd`,`permissions`,`query_template_def`,`reference_data`,`roles`,`rules`,`stats`,`text_data`,`transaction_log`,`units_lin_con`,`user_info`,`user_roles`','');
-
 -- ***************************************************************
 -- TABLE _caosdb_schema_unit_tests.collection_type
 -- ***************************************************************
@@ -104,7 +102,7 @@ SELECT tap.index_is_type('_caosdb_schema_unit_tests','collection_type','property
 SELECT tap.is_indexed('_caosdb_schema_unit_tests','collection_type','`property_id`','');
 
 -- CONSTRAINTS
-SELECT tap.constraints_are('_caosdb_schema_unit_tests','collection_type','`collection_type_domain_id_entity`,`collection_type_entity_id_entity`,`collection_type_property_id_entity`','');
+SELECT tap.constraints_are('_caosdb_schema_unit_tests','collection_type','`collection_type-d-e-p`,`collection_type_domain_id_entity`,`collection_type_entity_id_entity`,`collection_type_property_id_entity`','');
 
 -- CONSTRAINT collection_type.collection_type_domain_id_entity
 
@@ -1579,7 +1577,7 @@ SELECT tap.table_collation_is('_caosdb_schema_unit_tests','reference_data','utf8
 SELECT tap.table_engine_is('_caosdb_schema_unit_tests','reference_data','InnoDB','');
 
 -- COLUMNS
-SELECT tap.columns_are('_caosdb_schema_unit_tests','reference_data','`domain_id`,`entity_id`,`property_id`,`value`,`status`,`pidx`','');
+SELECT tap.columns_are('_caosdb_schema_unit_tests','reference_data','`domain_id`,`entity_id`,`property_id`,`value`,`status`,`pidx`,`value_iversion`','');
 
 -- COLUMN reference_data.domain_id
 
@@ -1635,8 +1633,17 @@ SELECT tap.col_default_is('_caosdb_schema_unit_tests','reference_data','pidx',0,
 SELECT tap.col_charset_is('_caosdb_schema_unit_tests','reference_data','pidx',NULL,'');
 SELECT tap.col_collation_is('_caosdb_schema_unit_tests','reference_data','pidx',NULL,'');
 
+-- COLUMN reference_data.value_iversion
+
+SELECT tap.has_column('_caosdb_schema_unit_tests','reference_data','value_iversion','');
+SELECT tap.col_column_type_is('_caosdb_schema_unit_tests','reference_data','value_iversion','int(10) unsigned','');
+SELECT tap.col_extra_is('_caosdb_schema_unit_tests','reference_data','value_iversion','','');
+SELECT tap.col_default_is('_caosdb_schema_unit_tests','reference_data','value_iversion','NULL','');
+SELECT tap.col_charset_is('_caosdb_schema_unit_tests','reference_data','value_iversion',NULL,'');
+SELECT tap.col_collation_is('_caosdb_schema_unit_tests','reference_data','value_iversion',NULL,'');
+
 -- INDEXES
-SELECT tap.indexes_are('_caosdb_schema_unit_tests','reference_data','`entity_id`','');
+SELECT tap.indexes_are('_caosdb_schema_unit_tests','reference_data','`entity_id`,`value`','');
 
 -- INDEX reference_data.entity_id
 
@@ -1644,8 +1651,14 @@ SELECT tap.has_index('_caosdb_schema_unit_tests','reference_data','entity_id',''
 SELECT tap.index_is_type('_caosdb_schema_unit_tests','reference_data','entity_id','BTREE','');
 SELECT tap.is_indexed('_caosdb_schema_unit_tests','reference_data','`entity_id`,`property_id`','');
 
+-- INDEX reference_data.value
+
+SELECT tap.has_index('_caosdb_schema_unit_tests','reference_data','value','');
+SELECT tap.index_is_type('_caosdb_schema_unit_tests','reference_data','value','BTREE','');
+SELECT tap.is_indexed('_caosdb_schema_unit_tests','reference_data','`value`,`value_iversion`','');
+
 -- CONSTRAINTS
-SELECT tap.constraints_are('_caosdb_schema_unit_tests','reference_data','`ref_domain_id_entity`,`ref_entity_id_entity`,`ref_property_id_entity`,`ref_value_entity`','');
+SELECT tap.constraints_are('_caosdb_schema_unit_tests','reference_data','`ref_domain_id_entity`,`ref_entity_id_entity`,`ref_property_id_entity`,`ref_value_entity`,`reference_data_ibfk_1`','');
 
 -- CONSTRAINT reference_data.ref_domain_id_entity
 
@@ -1675,6 +1688,13 @@ SELECT tap.constraint_type_is('_caosdb_schema_unit_tests','reference_data','ref_
 SELECT tap.fk_on_delete('_caosdb_schema_unit_tests','reference_data','ref_value_entity','RESTRICT','');
 SELECT tap.fk_on_update('_caosdb_schema_unit_tests','reference_data','ref_value_entity','RESTRICT','');
 
+-- CONSTRAINT reference_data_ibfk_1
+SELECT tap.has_constraint('_caosdb_schema_unit_tests','reference_data','reference_data_ibfk_1','');
+SELECT tap.constraint_type_is('_caosdb_schema_unit_tests','reference_data','reference_data_ibfk_1','FOREIGN KEY','');
+SELECT tap.fk_on_delete('_caosdb_schema_unit_tests','reference_data','reference_data_ibfk_1','RESTRICT','');
+SELECT tap.fk_on_update('_caosdb_schema_unit_tests','reference_data','reference_data_ibfk_1','RESTRICT','');
+
+
 -- ***************************************************************
 -- TABLE _caosdb_schema_unit_tests.roles
 -- ***************************************************************
@@ -2206,10 +2226,102 @@ SELECT tap.fk_on_delete('_caosdb_schema_unit_tests','user_roles','user_roles_ibf
 SELECT tap.fk_on_update('_caosdb_schema_unit_tests','user_roles','user_roles_ibfk_1','RESTRICT','');
 
 -- ***************************************************************
--- FUNCTIONS 
+-- TABLE _caosdb_schema_unit_tests.entity_version
 -- ***************************************************************
 
-SELECT tap.routines_are('_caosdb_schema_unit_tests','FUNCTION','`CaosDBVersion`,`convert_unit`,`constructDateTimeWhereClauseForColumn`,`getAggValueWhereClause`,`getDateTimeWhereClause`,`getDoubleWhereClause`,`getDateWhereClause`,`makeStmt`,`standard_unit`','');
+SELECT tap.has_table('_caosdb_schema_unit_tests','entity_version','');
+SELECT tap.table_collation_is('_caosdb_schema_unit_tests','entity_version','utf8_unicode_ci','');
+SELECT tap.table_engine_is('_caosdb_schema_unit_tests','entity_version','InnoDB','');
+
+-- COLUMNS
+SELECT tap.columns_are('_caosdb_schema_unit_tests','entity_version','`entity_id`,`hash`,`version`,`_iversion`,`_ipparent`,`srid`','');
+
+-- COLUMN entity_version.entity_id
+
+SELECT tap.has_column('_caosdb_schema_unit_tests','entity_version','entity_id','');
+SELECT tap.col_column_type_is('_caosdb_schema_unit_tests','entity_version','entity_id','int(10) unsigned','');
+SELECT tap.col_extra_is('_caosdb_schema_unit_tests','entity_version','entity_id','','');
+SELECT tap.col_default_is('_caosdb_schema_unit_tests','entity_version','entity_id',NULL,'');
+SELECT tap.col_charset_is('_caosdb_schema_unit_tests','entity_version','entity_id',NULL,'');
+SELECT tap.col_collation_is('_caosdb_schema_unit_tests','entity_version','entity_id',NULL,'');
+
+-- COLUMN entity_version.hash
+
+SELECT tap.has_column('_caosdb_schema_unit_tests','entity_version','hash','');
+SELECT tap.col_column_type_is('_caosdb_schema_unit_tests','entity_version','hash','varbinary(255)','');
+SELECT tap.col_extra_is('_caosdb_schema_unit_tests','entity_version','hash','','');
+SELECT tap.col_default_is('_caosdb_schema_unit_tests','entity_version','hash','NULL','');
+SELECT tap.col_charset_is('_caosdb_schema_unit_tests','entity_version','hash',NULL,'');
+SELECT tap.col_collation_is('_caosdb_schema_unit_tests','entity_version','hash',NULL,'');
+
+-- COLUMN entity_version.version
+
+SELECT tap.has_column('_caosdb_schema_unit_tests','entity_version','version','');
+SELECT tap.col_column_type_is('_caosdb_schema_unit_tests','entity_version','version','varbinary(255)','');
+SELECT tap.col_extra_is('_caosdb_schema_unit_tests','entity_version','version','','');
+SELECT tap.col_default_is('_caosdb_schema_unit_tests','entity_version','version',NULL,'');
+SELECT tap.col_charset_is('_caosdb_schema_unit_tests','entity_version','version',NULL,'');
+SELECT tap.col_collation_is('_caosdb_schema_unit_tests','entity_version','version',NULL,'');
+
+-- COLUMN entity_version._iversion
+
+SELECT tap.has_column('_caosdb_schema_unit_tests','entity_version','_iversion','');
+SELECT tap.col_column_type_is('_caosdb_schema_unit_tests','entity_version','_iversion','int(10) unsigned','');
+SELECT tap.col_extra_is('_caosdb_schema_unit_tests','entity_version','_iversion','','');
+SELECT tap.col_default_is('_caosdb_schema_unit_tests','entity_version','_iversion',NULL,'');
+SELECT tap.col_charset_is('_caosdb_schema_unit_tests','entity_version','_iversion',NULL,'');
+SELECT tap.col_collation_is('_caosdb_schema_unit_tests','entity_version','_iversion',NULL,'');
+
+-- COLUMN entity_version._ipparent
+
+SELECT tap.has_column('_caosdb_schema_unit_tests','entity_version','_ipparent','');
+SELECT tap.col_column_type_is('_caosdb_schema_unit_tests','entity_version','_ipparent','int(10) unsigned','');
+SELECT tap.col_extra_is('_caosdb_schema_unit_tests','entity_version','_ipparent','','');
+SELECT tap.col_default_is('_caosdb_schema_unit_tests','entity_version','_ipparent','NULL','');
+SELECT tap.col_charset_is('_caosdb_schema_unit_tests','entity_version','_ipparent',NULL,'');
+SELECT tap.col_collation_is('_caosdb_schema_unit_tests','entity_version','_ipparent',NULL,'');
+
+-- COLUMN entity_version.srid
+
+SELECT tap.has_column('_caosdb_schema_unit_tests','entity_version','srid','');
+SELECT tap.col_column_type_is('_caosdb_schema_unit_tests','entity_version','srid','varbinary(255)','');
+SELECT tap.col_extra_is('_caosdb_schema_unit_tests','entity_version','srid','','');
+SELECT tap.col_default_is('_caosdb_schema_unit_tests','entity_version','srid',NULL,'');
+SELECT tap.col_charset_is('_caosdb_schema_unit_tests','entity_version','srid',NULL,'');
+SELECT tap.col_collation_is('_caosdb_schema_unit_tests','entity_version','srid',NULL,'');
+
+-- CONSTRAINTS
+SELECT tap.constraints_are('_caosdb_schema_unit_tests','entity_version','`entity_version-e-v`,`PRIMARY`,`entity_version_ibfk_1`,`entity_version_ibfk_2`','');
+
+-- CONSTRAINT entity_version.entity_id
+
+SELECT tap.has_constraint('_caosdb_schema_unit_tests','entity_version','entity_version-e-v','');
+SELECT tap.constraint_type_is('_caosdb_schema_unit_tests','entity_version','entity_version-e-v','UNIQUE','');
+
+-- CONSTRAINT entity_version.PRIMARY
+
+SELECT tap.has_constraint('_caosdb_schema_unit_tests','entity_version','PRIMARY','');
+SELECT tap.constraint_type_is('_caosdb_schema_unit_tests','entity_version','PRIMARY','PRIMARY KEY','');
+SELECT tap.col_is_pk('_caosdb_schema_unit_tests','entity_version','`entity_id`,`_iversion`','');
+
+-- CONSTRAINT entity_version.entity_version_ibfk_1
+
+SELECT tap.has_constraint('_caosdb_schema_unit_tests','entity_version','entity_version_ibfk_1','');
+SELECT tap.constraint_type_is('_caosdb_schema_unit_tests','entity_version','entity_version_ibfk_1','FOREIGN KEY','');
+SELECT tap.fk_on_delete('_caosdb_schema_unit_tests','entity_version','entity_version_ibfk_1','CASCADE','');
+SELECT tap.fk_on_update('_caosdb_schema_unit_tests','entity_version','entity_version_ibfk_1','RESTRICT','');
+
+-- CONSTRAINT entity_version.entity_version_ibfk_2
+
+SELECT tap.has_constraint('_caosdb_schema_unit_tests','entity_version','entity_version_ibfk_2','');
+SELECT tap.constraint_type_is('_caosdb_schema_unit_tests','entity_version','entity_version_ibfk_2','FOREIGN KEY','');
+SELECT tap.fk_on_delete('_caosdb_schema_unit_tests','entity_version','entity_version_ibfk_2','RESTRICT','');
+SELECT tap.fk_on_update('_caosdb_schema_unit_tests','entity_version','entity_version_ibfk_2','RESTRICT','');
+
+
+-- ***************************************************************
+-- FUNCTIONS 
+-- ***************************************************************
 
 -- FUNCTION _caosdb_schema_unit_tests.CaosDBVersion
 
@@ -2287,8 +2399,6 @@ SELECT tap.function_sql_data_access_is('_caosdb_schema_unit_tests','standard_uni
 -- PROCEDURES 
 -- ***************************************************************
 
-SELECT tap.routines_are('_caosdb_schema_unit_tests','PROCEDURE','`applyBackReference`,`applyPOV`,`applyRefPOV`,`applyIDFilter`,`applySAT`,`applyTransactionFilter`,`calcComplementUnion`,`calcDifference`,`calcIntersection`,`calcUnion`,`cleanUpLinCon`,`cleanUpQuery`,`copyTable`,`createTmpTable`,`createTmpTable2`,`deleteEntity`,`deleteEntityProperties`,`deleteIsa`,`deleteLinCon`,`entityACL`,`finishNegationFilter`,`finishSubProperty`,`getChildren`,`getFileIdByPath`,`getRole`,`getRules`,`initAutoIncrement`,`initBackReference`,`getFile`,`initConjunctionFilter`,`initDisjunctionFilter`,`initNegationFilter`,`initPOVRefidsTable`,`initQuery`,`insertEntity`,`getDependentEntities`,`initEmptyTargetSet`,`initEntity`,`insertLinCon`,`insertUser`,`intersectTable`,`initPOVPropertiesTable`,`initSubEntity`,`isSubtype`,`overrideDesc`,`overrideName`,`overrideType`,`raiseWarning`,`registerSubdomain`,`initSubProperty`,`insertEntityProperty`,`registerTempTableName`,`retrieveDatatype`,`retrieveEntityParents`,`retrieveGroup`,`setPassword`,`insertIsa`,`retrieveEntity`,`retrieveEntityProperties`,`showEntityAutoIncr`,`updateLinCon`,`retrieveOverrides`,`updateEntity`','');
-
 -- PROCEDURES _caosdb_schema_unit_tests.applyBackReference
 
 SELECT tap.has_procedure('_caosdb_schema_unit_tests','applyBackReference','');
@@ -2485,13 +2595,6 @@ SELECT tap.procedure_is_deterministic('_caosdb_schema_unit_tests','initBackRefer
 SELECT tap.procedure_security_type_is('_caosdb_schema_unit_tests','initBackReference','DEFINER','');
 SELECT tap.procedure_sql_data_access_is('_caosdb_schema_unit_tests','initBackReference','CONTAINS SQL','');
 
--- PROCEDURES _caosdb_schema_unit_tests.getFile
-
-SELECT tap.has_procedure('_caosdb_schema_unit_tests','getFile','');
-SELECT tap.procedure_is_deterministic('_caosdb_schema_unit_tests','getFile','NO','');
-SELECT tap.procedure_security_type_is('_caosdb_schema_unit_tests','getFile','DEFINER','');
-SELECT tap.procedure_sql_data_access_is('_caosdb_schema_unit_tests','getFile','CONTAINS SQL','');
-
 -- PROCEDURES _caosdb_schema_unit_tests.initConjunctionFilter
 
 SELECT tap.has_procedure('_caosdb_schema_unit_tests','initConjunctionFilter','');
@@ -2729,5 +2832,20 @@ SELECT tap.has_procedure('_caosdb_schema_unit_tests','updateEntity','');
 SELECT tap.procedure_is_deterministic('_caosdb_schema_unit_tests','updateEntity','NO','');
 SELECT tap.procedure_security_type_is('_caosdb_schema_unit_tests','updateEntity','DEFINER','');
 SELECT tap.procedure_sql_data_access_is('_caosdb_schema_unit_tests','updateEntity','CONTAINS SQL','');
+
+-- PROCEDURES _caosdb_schema_unit_tests.insert_single_child_version
+
+SELECT tap.has_procedure('_caosdb_schema_unit_tests', 'insert_single_child_version', '');
+SELECT tap.procedure_is_deterministic('_caosdb_schema_unit_tests','insert_single_child_version','NO','');
+SELECT tap.procedure_security_type_is('_caosdb_schema_unit_tests','insert_single_child_version','DEFINER','');
+SELECT tap.procedure_sql_data_access_is('_caosdb_schema_unit_tests','insert_single_child_version','CONTAINS SQL','');
+
+-- PROCEDURES _caosdb_schema_unit_tests.delete_all_entity_versions
+
+SELECT tap.has_procedure('_caosdb_schema_unit_tests', 'delete_all_entity_versions', '');
+SELECT tap.procedure_is_deterministic('_caosdb_schema_unit_tests','delete_all_entity_versions','NO','');
+SELECT tap.procedure_security_type_is('_caosdb_schema_unit_tests','delete_all_entity_versions','DEFINER','');
+SELECT tap.procedure_sql_data_access_is('_caosdb_schema_unit_tests','delete_all_entity_versions','CONTAINS SQL','');
+
 CALL tap.finish();
 ROLLBACK;
diff --git a/tests/test_entity_versioning.sql b/tests/test_entity_versioning.sql
new file mode 100644
index 0000000000000000000000000000000000000000..0c5147c8dd4baab7cb2d323370ff2db8e18f3845
--- /dev/null
+++ b/tests/test_entity_versioning.sql
@@ -0,0 +1,315 @@
+/**
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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/>.
+ *
+ * ** end header
+ */
+
+USE _caosdb_schema_unit_tests;
+BEGIN;
+CALL tap.no_plan();
+
+
+
+-- #####################################################################
+-- TEST insert_single_child_version, get_primary_parent_version and
+-- delete_all_entity_versions in isolation
+-- #####################################################################
+
+-- SETUP
+
+-- Disable versioning because we want to test `insert_single_child_version`
+-- separately from `insertEntity` but the former is called inside the latter
+-- when versioning is enabled.
+DELETE FROM entity_version;
+DELETE FROM transactions;
+DELETE FROM feature_config WHERE _key = "ENTITY_VERSIONING";
+INSERT INTO transactions (srid,seconds,nanos,username,realm) VALUES
+("SRIDbla", 1234, 2345, "me", "home"),
+("SRIDblub", 2345, 3465, "me", "home"),
+("SRIDblieb", 3456, 4576, "you", "home");
+DELETE FROM entities WHERE id > 99;
+CALL entityACL(@ACLID1, "{acl1}");
+CALL insertEntity("EntityName", "EntityDesc", "RECORDTYPE", "{acl1}");
+SELECT entity_id INTO @EntityID FROM name_data WHERE value="EntityName";
+
+
+-- TEST insert_single_child_version
+SELECT count(*) INTO @x FROM entity_version;
+SELECT tap.eq(@x, 0, "no versions there yet");
+
+CALL insert_single_child_version(@EntityID, "hashbla", "versionbla", NULL, "SRIDbla");
+SELECT _ipparent INTO @x from entity_version WHERE version="versionbla";
+SELECT tap.eq(@x, NULL, "no parent for the first version");
+
+-- add a second version
+SELECT count(*) INTO @x FROM entity_version;
+SELECT tap.eq(@x, 1, "one version there already");
+CALL insert_single_child_version(@EntityID, "hashblub", "versionblub", "versionbla", "SRIDblub");
+
+SELECT _ipparent INTO @x from entity_version WHERE version="versionblub";
+SELECT tap.eq(@x, 1, "the original entity is the parent");
+
+-- error: parent does not exist
+SELECT count(*) INTO @x FROM entity_version;
+SELECT tap.eq(@x, 2, "two versions there already");
+
+CALL tap._assert_throws(
+     concat("CALL insert_single_child_version(",
+            @EntityID, ', "hashblieb", "versionblieb", "non-existing-parent", "SRIDBlieb")'),
+     "non existing parent throws");
+
+SELECT count(*) INTO @x FROM entity_version;
+SELECT tap.eq(@x, 2, "still two versions there");
+
+
+-- TEST get_primary_parent_version
+SELECT tap.eq(get_primary_parent_version(@EntityID, "versionblub"), "versionbla", "returns correct parent for versionblub");
+SELECT tap.eq(get_primary_parent_version(@EntityID, "versionbla"), NULL, "versionbla has no parent");
+
+
+-- TEST delete_all_entity_versions
+SELECT count(*) INTO @x FROM entity_version;
+SELECT tap.ok(@x > 0, "several versions in the table");
+
+CALL delete_all_entity_versions(@EntityID);
+SELECT count(*) INTO @x FROM entity_version;
+SELECT tap.eq(@x, 0, "no versions there any more");
+
+
+-- TEARDOWN clean up
+DELETE FROM name_data WHERE entity_id > 99;
+DELETE FROM entities WHERE id > 99;
+
+-- #####################################################################
+-- TEST the call of insert_single_child_version from within insertEntity
+-- #####################################################################
+
+-- SETUP switch on the feature
+INSERT INTO feature_config (_key, _value) VALUES ("ENTITY_VERSIONING", "ENABLED");
+SELECT count(*) INTO @x FROM entity_version;
+SELECT tap.eq(@x, 0, "before insertEntity, no versions there");
+
+-- TEST insertEntity - should produce a version w/o parent
+SET @SRID = "SRIDbla";
+CALL insertEntity("EntityName", "EntityDesc", "RECORDTYPE", "{acl1}");
+SELECT entity_id INTO @EntityID FROM name_data WHERE value="EntityName";
+CALL insertEntity("ParentName", "ParentDesc", "RECORDTYPE", "{acl1}");
+SELECT entity_id INTO @ParentID FROM name_data WHERE value="ParentName";
+CALL insertIsa(@EntityID, @ParentID);
+CALL insertEntityProperty(0, @EntityID, 17, "null_data", NULL, NULL,
+    "RECOMMENDED", NULL, "DescOverride", NULL, NULL, 0);
+
+SELECT count(*) INTO @x FROM entity_version WHERE entity_id = @EntityID;
+SELECT tap.eq(@x, 1, "after insertEntity, a version is there.");
+SELECT _iversion INTO @x FROM entity_version WHERE entity_id = @EntityID and _ipparent is NULL;
+SELECT tap.eq(@x, 1, "after insertEntity, the _iversion number is 1.");
+
+SELECT _ipparent INTO @x from entity_version WHERE entity_id = @EntityID;
+SELECT tap.eq(@x, NULL, "no parent for the freshly inserted entity");
+SELECT tap.eq(count(*), 0, "no entity in archive_entities before first update")
+    FROM archive_entities;
+
+
+-- TEST updateEntity - should produce a version with a parent
+SET @SRID = "SRIDblub";
+CALL deleteEntityProperties(@EntityID);
+CALL updateEntity(@EntityID, "NewEntityName", "NewEntityDesc", "RECORD", NULL, NULL, "{acl1}");
+SELECT count(*) INTO @x FROM entity_version WHERE entity_id = @EntityID;
+SELECT tap.eq(@x, 2, "after updateEntity, a second version is there.");
+
+SELECT _iversion INTO @x FROM entity_version WHERE entity_id = @EntityID and _ipparent = 1;
+SELECT tap.eq(@x, 2, "after updateEntity, the _iversion number incremented.");
+SELECT _ipparent INTO @x FROM entity_version WHERE entity_id = @EntityID and _ipparent = 1;
+SELECT tap.eq(@x, 1, "after updateEntity, the _pparent points to the first version");
+
+SELECT tap.eq(count(*), 1, "after updateEntity, one entity in archive_entities")
+    FROM archive_entities;
+
+
+-- TEST retrieveEntity
+
+SELECT version INTO @x FROM entity_version
+    WHERE entity_id = @EntityID
+    AND _iversion = 2;
+CALL retrieveEntity(@EntityID, NULL);
+CALL retrieveEntity(@EntityID, "non-existing-version");
+CALL retrieveEntity(@EntityID, get_head_version(@EntityID));
+CALL retrieveEntity(@EntityID, @x);
+
+-- TEST retrieveEntityParents
+
+CALL retrieveEntityParents(@EntityID, NULL);
+CALL retrieveEntityParents(@EntityID, "non-existing-version");
+CALL retrieveEntityParents(@EntityID, get_head_version(@EntityID));
+CALL retrieveEntityParents(@EntityID, @x);
+
+-- TEST retrieveEntityProperties
+
+CALL retrieveEntityProperties(0, @EntityID, NULL);
+CALL retrieveEntityProperties(0, @EntityID, "non-existing-version");
+CALL retrieveEntityProperties(0, @EntityID, get_head_version(@EntityID));
+CALL retrieveEntityProperties(0, @EntityID, @x);
+
+-- TEST retrieveOverrides
+
+CALL retrieveOverrides(0, @EntityID, NULL);
+CALL retrieveOverrides(0, @EntityID, "non-existing-version");
+CALL retrieveOverrides(0, @EntityID, get_head_version(@EntityID));
+CALL retrieveOverrides(0, @EntityID, @x);
+
+
+-- and 2nd updateEntity
+SET @SRID = "SRIDblieb";
+CALL updateEntity(@EntityID, "EntityName", "EntityDesc", "RECORDTYPE", NULL, NULL, "{acl1}");
+SELECT count(*) INTO @x FROM entity_version WHERE entity_id = @EntityID;
+SELECT tap.eq(@x, 3, "after 2nd updateEntity, a 3rd version is there.");
+
+SELECT _iversion INTO @x FROM entity_version WHERE entity_id = @EntityID and _ipparent = 2;
+SELECT tap.eq(@x, 3, "after 2nd updateEntity, the _iversion number incremented again.");
+SELECT _ipparent INTO @x FROM entity_version WHERE entity_id = @EntityID and _iversion = 3;
+SELECT tap.eq(@x, 2, "after 2nd updateEntity, the _pparent points to the 2nd version");
+SELECT tap.eq("SRIDblieb", srid, "correct transaction was stored") FROM entity_version WHERE entity_id = @EntityID AND _ipparent = 2;
+
+
+-- TEST deleteEntity - should remove all versions
+CALL deleteIsa(@EntityID);
+CALL deleteEntity(@EntityID);
+SELECT count(*) INTO @x FROM entity_version WHERE entity_id = @EntityID;
+SELECT tap.eq(@x, 0, "no versions there any more");
+
+CALL deleteEntity(@ParentID);
+
+CALL tap.finish();
+ROLLBACK;
+
+-- TEST _fix_unversioned
+-- turn off version, insert entity, turn on version, call _fix_unversioned
+-- The inserted entity should have no entry in entity_version at first and
+-- after _fix_unversioned an entry is gerated.
+
+BEGIN;
+CALL tap.no_plan();
+
+
+-- setup
+DELETE FROM entity_version;
+DELETE FROM transactions;
+-- insert entity without versioning
+DELETE FROM feature_config WHERE _key = "ENTITY_VERSIONING";
+CALL entityACL(@ACLID1, "{acl1}");
+CALL insertEntity("EntityName", "EntityDesc", "RECORDTYPE", "{acl1}");
+SELECT count(*) INTO @NumOfEntities FROM entities;
+SELECT entity_id INTO @EntityID FROM name_data WHERE value="EntityName";
+
+SET @TheUser = "TheUser"; -- used to identify the matching entry in transaction_log
+-- fill transaction_log: one entity with two updates (and one insert) and another entity with insert and delete.
+INSERT INTO transaction_log (transaction, entity_id, username, realm, seconds, nanos)
+    -- the first entry is the one which is be found by _fix_unversioned
+    VALUES
+        ("Update", @EntityID, @TheUser,     "CaosDB",  23458, 254),
+        -- the rest is dummy data
+        ("Update", @EntityID, "OtherUser",  "CaosDB",   2345, 633), -- not the latest transaction
+        ("Insert", @EntityID, "OtherUser",  "CaosDB",    245, 633), -- not the latest transaction
+        ("Insert", @EntityID + 1, @TheUser, "CaosDB",   2325, 653), -- not the right entity, inserted before our target
+        ("Delete", @EntityID + 1, @TheUser, "CaosDB", 232526, 653); -- not the right entity, deleted after our target
+
+
+SELECT tap.eq(COUNT(*), 5, "five entries in transaction_log") FROM transaction_log;
+SELECT tap.eq(COUNT(*), 0, "no versioned entity there") FROM entity_version;
+SELECT tap.eq(COUNT(*), 0, "no transaction there") FROM transactions;
+
+INSERT INTO feature_config (_key, _value) VALUES ("ENTITY_VERSIONING", "ENABLED");
+SELECT tap.eq(COUNT(*), 0, "still, no versioned entity there") FROM entity_version;
+SELECT tap.eq(COUNT(*), 0, "still, no transaction there") FROM transactions;
+
+-- call _fix_unversioned
+CALL _fix_unversioned();
+SELECT tap.eq(COUNT(*), @NumOfEntities,
+    "after _fix_unversioned, one entry for our test entity in entity_version")
+    FROM entity_version;
+SELECT tap.eq(COUNT(*), 2,
+    "after _fix_unversioned, one entry for our test entity in transactions, one for the standard entities.")
+    FROM transactions;
+
+SELECT tap.eq(entity_id, @EntityID, "versioned entity has correct id") FROM entity_version WHERE entity_id > 99;
+SELECT tap.ok(srid IS NOT NULL, "srid was generated and user/time matches entries from transaction_log")
+    FROM transactions AS t JOIN transaction_log AS l
+    ON (l.seconds = t.seconds AND l.nanos = t.nanos AND l.username = t.username AND l.realm = t.realm)
+    WHERE l.entity_id = @EntityID AND l.username = @TheUser;
+
+CALL tap.finish();
+ROLLBACK;
+
+
+-- TEST _fix_unversioned 2
+-- Nastier case: two entities. together inserted, one got updated afterwards.
+-- Both should be in the entity_version table after execution _fix_unversioned.
+BEGIN;
+CALL tap.no_plan();
+
+
+-- setup
+DELETE FROM entity_version;
+DELETE FROM transactions;
+-- insert entity without versioning
+DELETE FROM feature_config WHERE _key = "ENTITY_VERSIONING";
+
+CALL entityACL(@ACLID1, "{acl1}");
+CALL insertEntity("EntityName1", "EntityDesc1", "RECORDTYPE", "{acl1}");
+CALL insertEntity("EntityName2", "EntityDesc2", "RECORDTYPE", "{acl1}");
+SELECT count(*) INTO @NumOfEntities FROM entities;
+SELECT entity_id INTO @EntityID1 FROM name_data WHERE value="EntityName1";
+SELECT entity_id INTO @EntityID2 FROM name_data WHERE value="EntityName2";
+
+INSERT INTO transaction_log (transaction, entity_id, username, realm, seconds,
+        nanos)
+    -- the first entry is the one which will be found by _fix_unversioned
+    VALUES ("INSERT", @EntityID1, "User", "CaosDB", 10000, 250),
+        ("INSERT", @EntityID2, "User", "CaosDB", 10000, 250),
+        ("UPDATE", @EntityID2, "User", "CaosDB", 20000, 250);
+
+
+SELECT tap.eq(COUNT(*), 3, "three entries in transaction_log") FROM transaction_log;
+SELECT tap.eq(COUNT(*), 0, "no versioned entity there") FROM entity_version;
+SELECT tap.eq(COUNT(*), 0, "no transaction there") FROM transactions;
+
+INSERT INTO feature_config (_key, _value) VALUES ("ENTITY_VERSIONING", "ENABLED");
+SELECT tap.eq(COUNT(*), 0, "still, no versioned entity there") FROM entity_version;
+SELECT tap.eq(COUNT(*), 0, "still, no transaction there") FROM transactions;
+
+-- call _fix_unversioned
+CALL _fix_unversioned();
+SELECT tap.eq(COUNT(*), @NumOfEntities,
+        "after _fix_unversioned, one entry for our test entity in entity_version")
+    FROM entity_version;
+SELECT tap.eq(COUNT(*), 3,
+        "after _fix_unversioned, one entry for each of our test entities in
+        transactions, one for the standard entities.")
+    FROM transactions;
+
+SELECT tap.eq(seconds, 10000, "version seconds of entity 1 is correct")
+    FROM entity_version AS v JOIN transactions AS t
+    ON (v.srid = t.srid) WHERE v.entity_id = @EntityID1;
+SELECT tap.eq(seconds, 20000, "version seconds of entity 2 is correct")
+    FROM entity_version AS v JOIN transactions AS t
+    ON (v.srid = t.srid) WHERE v.entity_id = @EntityID2;
+
+CALL tap.finish();
+ROLLBACK;
diff --git a/tests/test_insert_update_delete.sql b/tests/test_insert_update_delete.sql
index 00b119444a38a270ae06e9b15226b4c1f991c920..a11b05e3d9a72d6038800adebf5b73c699c2a2b8 100644
--- a/tests/test_insert_update_delete.sql
+++ b/tests/test_insert_update_delete.sql
@@ -4,6 +4,9 @@ CALL tap.no_plan();
 
 -- SETUP
 
+-- Disable versioning and only test the non-versioning behavior
+DELETE FROM feature_config WHERE _key = "ENTITY_VERSIONING";
+
 CALL entityACL(@ACLID1, "{acl1}");
 CALL entityACL(@ACLID2, "{acl2}");
 SELECT entity_id into @TextDatatypeID FROM name_data WHERE value ="TEXT";
@@ -25,9 +28,7 @@ SELECT tap.eq(acl, @ACLID1, "correct acl id had been assigned")
     FROM entities WHERE id=@EntityID;
 
 
-
 -- TEST insertEntityProperty
-
 CALL insertEntity("AProperty", "APropDesc", "PROPERTY", "{acl1}");
 SELECT entity_id INTO @PropID FROM name_data WHERE value="AProperty";
 INSERT INTO data_type (domain_id, entity_id, property_id, datatype) VALUES (0, 0, @PropID, @TextDatatypeID);
@@ -39,7 +40,6 @@ SELECT COUNT(*) INTO @x FROM null_data;
 SELECT tap.eq(@x, 1, "One row in null_data table");
 
 -- TEST updateEntity
-
 CALL updateEntity(@EntityID, "NewEntityName", "NewEntityDesc", "RECORD", NULL, NULL, "{acl2}");
 
 SELECT tap.eq(COUNT(entity_id), 0, "Old Entity name not present")
@@ -56,20 +56,17 @@ CALL updateEntity(@EntityID, "NewEntityName", "NewEntityDesc", "RECORD", NULL, N
 
 
 -- TEST deleteEntityProperties
-
 CALL deleteEntityProperties(@EntityID);
 SELECT COUNT(*) INTO @x FROM null_data;
 SELECT tap.eq(@x, 0, "data removed from null_data table");
 
 -- TEST deleteEntity
-
 CALL deleteEntity(@EntityID);
 CALL deleteEntity(@PropID);
 SELECT COUNT(id) INTO @x FROM entities WHERE id>100;
 SELECT tap.eq(@x, 0, "entity deleted");
 
 
-
 -- TESTS END
 
 CALL tap.finish();
diff --git a/tests/test_reference_values.sql b/tests/test_reference_values.sql
new file mode 100644
index 0000000000000000000000000000000000000000..47b4db4d33cd7e841da610aca886a3d6bd4947d3
--- /dev/null
+++ b/tests/test_reference_values.sql
@@ -0,0 +1,137 @@
+/**
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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/>.
+ *
+ * ** end header
+ */
+
+USE _caosdb_schema_unit_tests;
+
+BEGIN;
+CALL tap.no_plan();
+
+-- #####################################################################
+-- TEST insertEntityProperty without Versioning
+-- #####################################################################
+
+-- SETUP
+DELETE FROM entity_version;
+DELETE FROM transactions;
+INSERT INTO transactions (srid,seconds,nanos,username,realm) VALUES
+("SRIDbla", 1234, 2345, "me", "home"),
+("SRIDblub", 2345, 3465, "me", "home"),
+("SRIDblieb", 3456, 4576, "you", "home");
+SET @EntityID=99;
+SET @PropertyID=11;
+SET @Value=50;
+
+-- switch off versioning
+DELETE FROM feature_config WHERE _key = "ENTITY_VERSIONING";
+
+-- TEST insertEntityProperty without Versioning
+CALL insertEntityProperty(0, @EntityID, @PropertyID, "reference_data", @Value,
+    NULL, "FIX", NULL, NULL, NULL, NULL, 0);
+
+-- TODO switch expected/actual
+SELECT tap.eq(0, domain_id, "domain ok") FROM reference_data;
+SELECT tap.eq(99, entity_id, "entity ok") FROM reference_data;
+SELECT tap.eq(11, property_id, "property ok") FROM reference_data;
+SELECT tap.eq(50, value, "value ok") FROM reference_data;
+SELECT tap.eq("FIX", status, "status ok") FROM reference_data;
+SELECT tap.eq("0", pidx, "pidx ok") FROM reference_data;
+SELECT tap.eq(NULL, value_iversion, "value_iversion ok") FROM reference_data;
+
+-- clean up
+DELETE FROM reference_data WHERE domain_id=0 AND entity_id=99;
+
+-- #####################################################################
+-- TODO TEST insertEntityProperty with Versioning
+-- #####################################################################
+
+-- switch on versioning
+INSERT INTO feature_config (_key, _value) VALUES ("ENTITY_VERSIONING", "ENABLED");
+
+
+-- TEST insertEntityProperty with Versioning - REFERENCE HEAD
+SET @VALUE="50";
+CALL insertEntityProperty(0, @EntityID, @PropertyID, "reference_data", @Value,
+    NULL, "FIX", NULL, NULL, NULL, NULL, 0);
+
+-- TODO switch expected/actual
+SELECT tap.eq(0, domain_id, "domain ok") FROM reference_data;
+SELECT tap.eq(99, entity_id, "entity ok") FROM reference_data;
+SELECT tap.eq(11, property_id, "property ok") FROM reference_data;
+SELECT tap.eq(50, value, "value ok") FROM reference_data;
+SELECT tap.eq("FIX", status, "status ok") FROM reference_data;
+SELECT tap.eq("0", pidx, "pidx ok") FROM reference_data;
+SELECT tap.eq(value_iversion, NULL, "value_iversion ok") FROM reference_data;
+
+DELETE FROM reference_data WHERE domain_id=0 AND entity_id=99;
+
+
+-- TEST insertEntityProperty with Versioning - Reference version
+CALL insert_single_child_version(50, "hashbla", "versionbla", NULL, "SRIDbla");
+CALL insert_single_child_version(50, "hashblub", "versionblub", "versionbla", "SRIDblub");
+
+SET @VALUE="50@versionbla";
+CALL insertEntityProperty(0, @EntityID, @PropertyID, "reference_data", @Value,
+    NULL, "FIX", NULL, NULL, NULL, NULL, 0);
+
+SELECT tap.eq(0, domain_id, "domain ok") FROM reference_data;
+SELECT tap.eq(99, entity_id, "entity ok") FROM reference_data;
+SELECT tap.eq(11, property_id, "property ok") FROM reference_data;
+SELECT tap.eq(50, value, "value ok") FROM reference_data;
+SELECT tap.eq("FIX", status, "status ok") FROM reference_data;
+SELECT tap.eq("0", pidx, "pidx ok") FROM reference_data;
+SELECT tap.eq(value_iversion, "1", "value_iversion ok") FROM reference_data;
+
+DELETE FROM reference_data WHERE domain_id=0 AND entity_id=99;
+
+SET @VALUE="50@versionblub";
+CALL insertEntityProperty(0, @EntityID, @PropertyID, "reference_data", @Value,
+    NULL, "FIX", NULL, NULL, NULL, NULL, 0);
+
+-- TODO switch expected/actual
+SELECT tap.eq(0, domain_id, "domain ok") FROM reference_data;
+SELECT tap.eq(99, entity_id, "entity ok") FROM reference_data;
+SELECT tap.eq(11, property_id, "property ok") FROM reference_data;
+SELECT tap.eq(50, value, "value ok") FROM reference_data;
+SELECT tap.eq("FIX", status, "status ok") FROM reference_data;
+SELECT tap.eq("0", pidx, "pidx ok") FROM reference_data;
+SELECT tap.eq(value_iversion, "2", "value_iversion ok") FROM reference_data;
+
+
+-- invalid values throw errors
+SET @VALUE="50@";
+CALL tap._assert_throws('
+    CALL insertEntityProperty(0, @EntityID, @PropertyID, "reference_data", @Value,
+        NULL, "FIX", NULL, NULL, NULL, NULL, 0)', "@ but no version id");
+
+SET @VALUE="50@non-existing-version";
+CALL tap._assert_throws('
+    CALL insertEntityProperty(0, @EntityID, @PropertyID, "reference_data", @Value,
+        NULL, "FIX", NULL, NULL, NULL, NULL, 0)', "non-existing-version id");
+
+
+-- #####################################################################
+-- TEST retrieveEntityProperty without Versioning
+-- #####################################################################
+
+CALL tap.finish();
+ROLLBACK;
diff --git a/utils/make_db b/utils/make_db
index e1cdc2f8673de6e043a67e85106f40c7c655c2f0..93a62ea2b9e6776a73b8298cbce1a54aa76e0f68 100755
--- a/utils/make_db
+++ b/utils/make_db
@@ -56,12 +56,16 @@ function _execute_tests () {
     for tfile in $TESTS ; do
         echo "Running $tfile"
         echo "----- $tfile -----" >> .TEST_RESULTS
-        cat $tfile | $MYSQL_CMD $(get_db_args_nodb) --disable-pager --batch --raw --skip-column-names --unbuffered >> .TEST_RESULTS
+        < $tfile $MYSQL_CMD \
+          -D $UNITTEST_DATABASE $(get_db_args_nodb) --disable-pager \
+          --batch --raw --skip-column-names --unbuffered \
+          >> .TEST_RESULTS 2>&1
     done;
 
     popd
     cat tests/.TEST_RESULTS
     grep -c -i "failed" tests/.TEST_RESULTS > /dev/null && return 1
+    grep -c -i "ERROR" tests/.TEST_RESULTS > /dev/null && return 1
     return 0
 }
 
@@ -69,18 +73,21 @@ function _execute_tests () {
 # optional parameter: [--fresh] for installing a fresh data base. Otherwise an existing one would be reused.
 function _install_unit_test_database () {
     DATABASE_NAME=$UNITTEST_DATABASE
-    if _db_exists "$DATABASE_NAME"; then
-        if [[ "$1" == "--fresh" ]] ; then
+    if [[ $1 = "--fresh" ]] ; then
+        if _db_exists "$DATABASE_NAME"; then
             drop "$DATABASE_NAME" ;
-        else
-            return 0;
         fi
     fi
 
-    sed "s/db_2_0/$DATABASE_NAME/g" "$INSTALL_SQL_FILE" | $MYSQL_CMD $(get_mysql_args_nodb)
+    if _db_exists "$DATABASE_NAME"; then
+        echo "using $DATABASE_NAME"
+    else
+        sed "s/db_2_0/$DATABASE_NAME/g" "$INSTALL_SQL_FILE" | $MYSQL_CMD $(get_mysql_args_nodb)
+
+        # create test user
+        grant
+    fi
 
-    # create test user
-    grant
 
     echo "DATABASE_NAME=\"$UNITTEST_DATABASE\"" >> .test_config
     pushd patches > /dev/null
@@ -140,7 +147,7 @@ function grant() {
             echo
             read -p "Please repeat the password: " -s password2
             echo
-            [[ "$password" == "$password2" ]] && {
+            [[ "$password" = "$password2" ]] && {
                 DATABASE_USER_PW="$password"
                 break
             }
@@ -148,10 +155,10 @@ function grant() {
         done
     fi
 
-    if [[ "$1" == "--strict" ]] ; then
+    if [[ $1 = "--strict" ]] ; then
         for host in ${DATABASE_USER_HOST_LIST//,/ } ; do
             CMD="SELECT COUNT(*) FROM mysql.user WHERE user='${DATABASE_USER}' AND host='${host}';"
-            [[ $($MYSQL_CMD $(get_mysql_args_nodb) -s -N -e "$CMD") == 0 ]] || {
+            [[ $($MYSQL_CMD $(get_mysql_args_nodb) -s -N -e "$CMD") = 0 ]] || {
                 echo "The user '${DATABASE_USER}@${host}' is already in the database."
                 echo "Please use another user or delete it, e.g. with"
                 echo "'mysql -u ${MYSQL_USER} -p -e \"DROP USER ${DATABASE_USER}@${host};\"'"
diff --git a/utils/patch_header.sh b/utils/patch_header.sh
index c50846ed2a03677c86ccd056dab111a04a8f4903..33fbff879b3166596e153685105873adeae8405b 100644
--- a/utils/patch_header.sh
+++ b/utils/patch_header.sh
@@ -131,6 +131,10 @@ function update_version {
 }
 
 function dump_table {
+    if [[ -z $MYSQLDUMP_CMD ]]; then
+        echo "Cannot find mysqldump program!" >&2
+        exit 2
+    fi
     $MYSQLDUMP_CMD $(get_db_args_nodb) $DATABASE_NAME $1 \
                     > ${DATABASE_NAME}.${1}.${OLD_VERSION}.dump.sql
 }