From 108037777606d46cbe4d3b87b9be7bf4fb1b855d Mon Sep 17 00:00:00 2001
From: Martin Weise <martin.weise@tuwien.ac.at>
Date: Sun, 26 Nov 2023 09:21:00 +0100
Subject: [PATCH] Finalized the semantic mapping

---
 dbrepo-metadata-db/setup-schema.sql           |  4 +-
 .../database/table/columns/TableColumn.java   |  4 +-
 .../table/columns/TableColumnConcept.java     |  2 +-
 .../table/columns/TableColumnUnit.java        |  4 +-
 .../tuwien/endpoints/TableColumnEndpoint.java |  2 +-
 .../service/impl/SemanticServiceImpl.java     | 21 +++---
 .../tuwien/service/impl/TableServiceImpl.java | 10 +--
 dbrepo-search-service/app/api/routes.py       |  2 -
 .../app/opensearch_client.py                  | 15 ++++-
 dbrepo-ui/api/database.service.js             | 67 +++++--------------
 dbrepo-ui/api/index.js                        |  7 +-
 dbrepo-ui/api/search.service.js               |  4 ++
 dbrepo-ui/api/semantic.mapper.js              | 17 +++++
 dbrepo-ui/api/user.service.js                 | 33 +++------
 .../components/search/AdvancedSearch.vue      | 61 ++++++++++++++++-
 15 files changed, 152 insertions(+), 101 deletions(-)
 create mode 100644 dbrepo-ui/api/semantic.mapper.js

diff --git a/dbrepo-metadata-db/setup-schema.sql b/dbrepo-metadata-db/setup-schema.sql
index a24971600a..e207d09e44 100644
--- a/dbrepo-metadata-db/setup-schema.sql
+++ b/dbrepo-metadata-db/setup-schema.sql
@@ -304,7 +304,7 @@ CREATE TABLE IF NOT EXISTS `mdb_columns_concepts`
     id      bigint    NOT NULL,
     cID     bigint    NOT NULL,
     created timestamp NOT NULL DEFAULT NOW(),
-    PRIMARY KEY (id),
+    PRIMARY KEY (id, cid),
     FOREIGN KEY (cID) REFERENCES mdb_columns (ID)
 ) WITH SYSTEM VERSIONING;
 
@@ -313,7 +313,7 @@ CREATE TABLE IF NOT EXISTS `mdb_columns_units`
     id      bigint    NOT NULL,
     cID     bigint    NOT NULL,
     created timestamp NOT NULL DEFAULT NOW(),
-    PRIMARY KEY (id),
+    PRIMARY KEY (id, cID),
     FOREIGN KEY (cID) REFERENCES mdb_columns (ID)
 ) WITH SYSTEM VERSIONING;
 
diff --git a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumn.java b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumn.java
index 6a0ecd6de9..8c4dd227c1 100644
--- a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumn.java
+++ b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumn.java
@@ -88,13 +88,13 @@ public class TableColumn implements Comparable<TableColumn> {
     @CreatedDate
     private Instant created;
 
-    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
+    @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.PERSIST})
     @JoinTable(name = "mdb_columns_concepts",
             joinColumns = @JoinColumn(name = "cid", referencedColumnName = "id", nullable = false),
             inverseJoinColumns = @JoinColumn(name = "id", referencedColumnName = "id"))
     private TableColumnConcept concept;
 
-    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
+    @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.PERSIST})
     @JoinTable(name = "mdb_columns_units",
             joinColumns = @JoinColumn(name = "cid", referencedColumnName = "id", nullable = false),
             inverseJoinColumns = @JoinColumn(name = "id", referencedColumnName = "id"))
diff --git a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnConcept.java b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnConcept.java
index ca9853256d..5d6edc84d8 100644
--- a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnConcept.java
+++ b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnConcept.java
@@ -47,7 +47,7 @@ public class TableColumnConcept {
     private Instant created;
 
     @ToString.Exclude
-    @OneToMany(fetch = FetchType.LAZY)
+    @OneToMany(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE})
     @org.springframework.data.annotation.Transient
     @JoinTable(name = "mdb_columns_concepts",
             inverseJoinColumns = {
diff --git a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnUnit.java b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnUnit.java
index 8f115bdf13..01cb3a0faa 100644
--- a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnUnit.java
+++ b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnUnit.java
@@ -47,9 +47,9 @@ public class TableColumnUnit {
     private Instant created;
 
     @ToString.Exclude
-    @OneToMany(fetch = FetchType.LAZY)
+    @OneToMany(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE})
     @org.springframework.data.annotation.Transient
-    @JoinTable(name = "mdb_columns_concepts",
+    @JoinTable(name = "mdb_columns_units",
             inverseJoinColumns = {
                     @JoinColumn(name = "cid", referencedColumnName = "id", insertable = false, updatable = false)
             },
diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java
index ebe9b7375d..4b515952b6 100644
--- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java
+++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java
@@ -87,7 +87,7 @@ public class TableColumnEndpoint {
             throws TableNotFoundException, TableMalformedException, DatabaseNotFoundException,
             ContainerNotFoundException, NotAllowedException, SemanticEntityPersistException,
             SemanticEntityNotFoundException, QueryMalformedException, AccessDeniedException {
-        log.debug("endpoint update table, id={}, tableId={}, {}", id, tableId, PrincipalUtil.formatForDebug(principal));
+        log.debug("endpoint update table, id={}, tableId={}, columnId={}, {}", id, tableId, columnId, PrincipalUtil.formatForDebug(principal));
         if (principal != null && !UserUtil.hasRole(principal, "modify-foreign-table-column-semantics")) {
             endpointValidator.validateOnlyAccess(id, principal, true);
             endpointValidator.validateOnlyOwnerOrWriteAll(id, tableId, principal);
diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/SemanticServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/SemanticServiceImpl.java
index 3841509198..4e0887db8e 100644
--- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/SemanticServiceImpl.java
+++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/SemanticServiceImpl.java
@@ -125,13 +125,13 @@ public class SemanticServiceImpl implements SemanticService {
                     .build();
         }
         /* save in metadata database */
-        final TableColumnConcept concept = tableMapper.entityDtoToTableColumnConcept(
-                entityService.findOneByUri(ontology, uri));
-        log.info("Saved concept with uri {} in metadata database", concept.getUri());
+        final TableColumnConcept concept = tableMapper.entityDtoToTableColumnConcept(entityService.findOneByUri(ontology, uri));
+        final TableColumnConcept entity = tableColumnConceptRepository.save(concept);
+        log.info("Saved concept with uri {} in metadata database", entity.getUri());
         /* save in open search database */
-        conceptIdxRepository.save(tableMapper.tableColumnConceptToConceptDto(concept));
-        log.info("Saved concept with uri {} in open search database", concept.getUri());
-        return concept;
+        conceptIdxRepository.save(tableMapper.tableColumnConceptToConceptDto(entity));
+        log.info("Saved concept with uri {} in open search database", entity.getUri());
+        return entity;
     }
 
     @Override
@@ -144,11 +144,12 @@ public class SemanticServiceImpl implements SemanticService {
         }
         /* save in metadata database */
         final TableColumnUnit unit = tableMapper.entityDtoToTableColumnUnit(entityService.findOneByUri(ontology, uri));
-        log.info("Saved unit with uri {} in metadata database", unit.getUri());
+        final TableColumnUnit entity = tableColumnUnitRepository.save(unit);
+        log.info("Saved unit with uri {} in metadata database", entity.getUri());
         /* save in open search database */
-        unitIdxRepository.save(tableMapper.tableColumnUnitToUnitDto(unit));
-        log.info("Saved unit with uri {} in open search database", unit.getUri());
-        return unit;
+        unitIdxRepository.save(tableMapper.tableColumnUnitToUnitDto(entity));
+        log.info("Saved unit with uri {} in open search database", entity.getUri());
+        return entity;
     }
 
     private Ontology getCompatibleOntology(String uri) {
diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java
index fa5e9d60f4..8330501cb2 100644
--- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java
+++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java
@@ -246,24 +246,26 @@ public class TableServiceImpl extends HibernateConnector implements TableService
         if (updateDto.getUnitUri() != null) {
             try {
                 column.setUnit(semanticService.findUnit(updateDto.getUnitUri()));
+                log.debug("Found unit with uri {} in metadata database", updateDto.getUnitUri());
             } catch (UnitNotFoundException e) {
-                log.warn("Unit with uri {} not found in metadata database", updateDto.getUnitUri());
                 column.setUnit(semanticService.saveUnit(updateDto.getUnitUri()));
+                log.info("Unit with uri {} was created in metadata database", updateDto.getUnitUri());
             }
         } else {
             column.setUnit(null);
-            log.debug("remove unit of column, column={}", column);
+            log.debug("remove unit of column with id={}", columnId);
         }
         if (updateDto.getConceptUri() != null) {
             try {
                 column.setConcept(semanticService.findConcept(updateDto.getConceptUri()));
+                log.debug("Found concept with uri {} in metadata database", updateDto.getConceptUri());
             } catch (ConceptNotFoundException e) {
-                log.warn("Concept with uri {} not found in metadata database", updateDto.getConceptUri());
                 column.setConcept(semanticService.saveConcept(updateDto.getConceptUri()));
+                log.info("Concept with uri {} was created in metadata database", updateDto.getConceptUri());
             }
         } else {
             column.setConcept(null);
-            log.debug("remove ColumnConcept of column, column={}", column);
+            log.debug("remove ColumnConcept of column with id={}", columnId);
         }
         /* update in metadata database */
         final TableColumn out = tableColumnRepository.save(column);
diff --git a/dbrepo-search-service/app/api/routes.py b/dbrepo-search-service/app/api/routes.py
index be2812da9d..b6d45b3091 100644
--- a/dbrepo-search-service/app/api/routes.py
+++ b/dbrepo-search-service/app/api/routes.py
@@ -154,8 +154,6 @@ def search():
     search_term = req_body.get("search_term")
     t1 = req_body.get("t1")
     t2 = req_body.get("t2")
-    field = req_body.get("field")
-    value = req_body.get("value")
     fieldValuePairs = req_body.get("field_value_pairs")
     response = general_search(search_term, t1, t2, fieldValuePairs)
     return response, 200
diff --git a/dbrepo-search-service/app/opensearch_client.py b/dbrepo-search-service/app/opensearch_client.py
index 2dcc1e0f7c..0b454b05d0 100644
--- a/dbrepo-search-service/app/opensearch_client.py
+++ b/dbrepo-search-service/app/opensearch_client.py
@@ -173,8 +173,19 @@ def general_search(search_term=None, t1=None, t2=None, fieldValuePairs=None):
         )
         response["status"] = 200
         return response
-    if t1 and t2 is not None:
-        logging.debug('query has time range present')
+    if t1 is not None:
+        logging.debug(f"query has start value {t1} present")
+        time_range_query = {
+            "range": {
+                "created": {
+                    "gte": t1,
+                    "lte": t2,
+                }
+            }
+        }
+        queries.append(time_range_query)
+    if t1 is not None and t2 is not None:
+        logging.debug(f"query has start value {t1} and end value {t2} present")
         time_range_query = {
             "range": {
                 "created": {
diff --git a/dbrepo-ui/api/database.service.js b/dbrepo-ui/api/database.service.js
index 7eb8f36f07..fa020ef185 100644
--- a/dbrepo-ui/api/database.service.js
+++ b/dbrepo-ui/api/database.service.js
@@ -1,5 +1,4 @@
-import Vue from 'vue'
-import api from '@/api'
+import api, { displayError } from '@/api'
 
 class DatabaseService {
   findAll () {
@@ -11,9 +10,7 @@ class DatabaseService {
           resolve(databases)
         })
         .catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to load databases', error)
-          Vue.$toast.error(`[${code}] Failed to load databases: ${message}`)
+          displayError(error, 'Failed to load databases')
           reject(error)
         })
     })
@@ -28,9 +25,7 @@ class DatabaseService {
           resolve(databases)
         })
         .catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to load my databases', error)
-          Vue.$toast.error(`[${code}] Failed to load my databases: ${message}`)
+          displayError(error, 'Failed to load my databases')
           reject(error)
         })
     })
@@ -45,9 +40,7 @@ class DatabaseService {
           resolve(count)
         })
         .catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to count databases', error)
-          Vue.$toast.error(`[${code}] Failed to count databases: ${message}`)
+          displayError(error, 'Failed to count databases')
           reject(error)
         })
     })
@@ -61,9 +54,7 @@ class DatabaseService {
           console.debug('response database', database)
           resolve(database)
         }).catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to load database', error)
-          Vue.$toast.error(`[${code}] Failed to load database: ${message}`)
+          displayError(error, 'Failed to load database')
           reject(error)
         })
     })
@@ -77,9 +68,7 @@ class DatabaseService {
           console.debug('response database', database)
           resolve(database)
         }).catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to create database', error)
-          Vue.$toast.error(`[${code}] Failed to create database: ${message}`)
+          displayError(error, 'Failed to create database')
           reject(error)
         })
     })
@@ -90,9 +79,7 @@ class DatabaseService {
       api.delete(`/api/database/${databaseId}`, { headers: { Accept: 'application/json' } })
         .then(() => resolve())
         .catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to delete database', error)
-          Vue.$toast.error(`[${code}] Failed to delete database: ${message}`)
+          displayError(error, 'Failed to delete database')
           reject(error)
         })
     })
@@ -106,9 +93,7 @@ class DatabaseService {
           console.debug('response database', database)
           resolve(database)
         }).catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to modify database visibility', error)
-          Vue.$toast.error(`[${code}] Failed to modify database visibility: ${message}`)
+          displayError(error, 'Failed to modify database visibility')
           reject(error)
         })
     })
@@ -122,9 +107,7 @@ class DatabaseService {
           console.debug('response database', database)
           resolve(database)
         }).catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to modify database owner', error)
-          Vue.$toast.error(`[${code}] Failed to modify database owner: ${message}`)
+          displayError(error, 'Failed to modify database owner')
           reject(error)
         })
     })
@@ -140,10 +123,8 @@ class DatabaseService {
         })
         .catch((error) => {
           const { status } = error
-          const { code, message } = error.response.data
           if (status !== 401 && status !== 403 && status !== 405) { /* ignore no access errors */
-            console.error('Failed to check database access', error)
-            Vue.$toast.error(`[${code}] Failed to check database access: ${message}`)
+            displayError(error, 'Failed to check database access')
             reject(error)
           }
         })
@@ -159,9 +140,7 @@ class DatabaseService {
           resolve(database)
         })
         .catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to modify database access', error)
-          Vue.$toast.error(`[${code}] Failed to modify database access: ${message}`)
+          displayError(error, 'Failed to modify database access')
           reject(error)
         })
     })
@@ -172,9 +151,7 @@ class DatabaseService {
       api.delete(`/api/database/${databaseId}/access/${userId}`, { headers: { Accept: 'application/json' } })
         .then(() => resolve())
         .catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to revoke database access', error)
-          Vue.$toast.error(`[${code}] Failed to revoke database access: ${message}`)
+          displayError(error, 'Failed to revoke database access')
           reject(error)
         })
     })
@@ -185,9 +162,7 @@ class DatabaseService {
       api.post(`/api/database/${databaseId}/access/${userId}`, { type }, { headers: { Accept: 'application/json' } })
         .then(() => resolve())
         .catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to give database access', error)
-          Vue.$toast.error(`[${code}] Failed to give database access: ${message}`)
+          displayError(error, 'Failed to give database access')
           reject(error)
         })
     })
@@ -202,9 +177,7 @@ class DatabaseService {
           resolve(licenses)
         })
         .catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to load licenses', error)
-          Vue.$toast.error(`[${code}] Failed to load licenses: ${message}`)
+          displayError(error, 'Failed to load licenses')
           reject(error)
         })
     })
@@ -219,9 +192,7 @@ class DatabaseService {
           resolve(view)
         })
         .catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to find view', error)
-          Vue.$toast.error(`[${code}] Failed to find view: ${message}`)
+          displayError(error, 'Failed to find view')
           reject(error)
         })
     })
@@ -236,9 +207,7 @@ class DatabaseService {
           resolve(view)
         })
         .catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to delete view', error)
-          Vue.$toast.error(`[${code}] Failed to delete view: ${message}`)
+          displayError(error, 'Failed to create view')
           reject(error)
         })
     })
@@ -249,9 +218,7 @@ class DatabaseService {
       api.delete(`/api/database/${databaseId}/view/${viewId}`, { headers: { Accept: 'application/json' } })
         .then(() => resolve())
         .catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to delete view', error)
-          Vue.$toast.error(`[${code}] Failed to delete view: ${message}`)
+          displayError(error, 'Failed to delete view')
           reject(error)
         })
     })
diff --git a/dbrepo-ui/api/index.js b/dbrepo-ui/api/index.js
index ed74b41a80..c56e45b3dd 100644
--- a/dbrepo-ui/api/index.js
+++ b/dbrepo-ui/api/index.js
@@ -13,8 +13,13 @@ const instance = axios.create({
 
 function displayError (error, warning) {
   const { code, message } = error.response.data
+  if (code && message) {
+    console.error(warning, error)
+    Vue.$toast.error(`[${code}] ${warning}: ${message}`)
+    return
+  }
   console.error(warning, error)
-  Vue.$toast.error(`[${code}] ${warning}: ${message}`)
+  Vue.$toast.error(`[${error.code}] ${warning}: ${error.message}`)
 }
 
 export default instance
diff --git a/dbrepo-ui/api/search.service.js b/dbrepo-ui/api/search.service.js
index 2dac62689f..faca4dd92a 100644
--- a/dbrepo-ui/api/search.service.js
+++ b/dbrepo-ui/api/search.service.js
@@ -23,8 +23,12 @@ class SearchService {
     // transform values to what the search API expects
     const searchTerm = searchData.search_term
     delete searchData.search_term
+    const t1 = searchData.t1
+    const t2 = searchData.t2
     searchData = Object.fromEntries(Object.entries(searchData).filter(([_, v]) => v != null && v !== '')) // https://stackoverflow.com/questions/286141/remove-blank-attributes-from-an-object-in-javascript
     const payload = {
+      t1,
+      t2,
       search_term: searchTerm,
       field_value_pairs: { ...searchData }
     }
diff --git a/dbrepo-ui/api/semantic.mapper.js b/dbrepo-ui/api/semantic.mapper.js
new file mode 100644
index 0000000000..79f71083bd
--- /dev/null
+++ b/dbrepo-ui/api/semantic.mapper.js
@@ -0,0 +1,17 @@
+class SemanticMapper {
+  mapConcepts (concepts) {
+    return concepts.map((concept) => {
+      concept.name = concept.name ? concept.name : concept.uri
+      return concept
+    })
+  }
+
+  mapUnits (units) {
+    return units.map((unit) => {
+      unit.name = unit.name ? unit.name : unit.uri
+      return unit
+    })
+  }
+}
+
+export default new SemanticMapper()
diff --git a/dbrepo-ui/api/user.service.js b/dbrepo-ui/api/user.service.js
index 0eacd13861..fd4cc2ba8b 100644
--- a/dbrepo-ui/api/user.service.js
+++ b/dbrepo-ui/api/user.service.js
@@ -1,5 +1,4 @@
-import Vue from 'vue'
-import api from '@/api'
+import api, { displayError } from '@/api'
 import UserMapper from '@/api/user.mapper'
 
 class UserService {
@@ -12,9 +11,7 @@ class UserService {
           resolve(users)
         })
         .catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to load users', error)
-          Vue.$toast.error(`[${code}] Failed to load users: ${message}`)
+          displayError(error, 'Failed to load users')
           reject(error)
         })
     })
@@ -28,9 +25,7 @@ class UserService {
           console.debug('response user', response.data, 'mapped user', user)
           resolve(user)
         }).catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to load user', error)
-          Vue.$toast.error(`[${code}] Failed to load user: ${message}`)
+          displayError(error, 'Failed to load user')
           reject(error)
         })
     })
@@ -44,9 +39,7 @@ class UserService {
           console.debug('response user', response.data, 'mapped user', user)
           resolve(user)
         }).catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to update user information', error)
-          Vue.$toast.error(`[${code}] Failed to update user information: ${message}`)
+          displayError(error, 'Failed to update user information')
           reject(error)
         })
     })
@@ -61,17 +54,15 @@ class UserService {
           resolve(user)
         }).catch((error) => {
           const { status } = error
-          const { code, message } = error.response.data
           if (status === 417) {
-            Vue.$toast.error('This e-mail address is already taken')
+            displayError(error, 'This e-mail address is already taken')
           } else if (status === 409) {
-            Vue.$toast.error('This username is already taken')
+            displayError(error, 'This username is already taken')
           } else if (status === 428) {
-            Vue.$toast.warning(`[${code}] Account was created: ${message}`)
+            displayError(error, 'Account was created')
           } else {
-            Vue.$toast.error(`[${code}] Failed to create user: ${message}`)
+            displayError(error, 'Failed to create user')
           }
-          console.error('Failed to create user', error)
           this.loading = false
           reject(error)
         })
@@ -83,9 +74,7 @@ class UserService {
       api.put(`/api/user/${id}/password`, { password }, { headers: { Accept: 'application/json' } })
         .then(() => resolve())
         .catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to update user password', error)
-          Vue.$toast.error(`[${code}] Failed to update user password: ${message}`)
+          displayError(error, 'Failed to update password')
           reject(error)
         })
     })
@@ -96,9 +85,7 @@ class UserService {
       api.put(`/api/user/${id}/theme`, { theme_dark: themeDark }, { headers: { Accept: 'application/json' } })
         .then(() => resolve())
         .catch((error) => {
-          const { code, message } = error.response.data
-          console.error('Failed to update user theme', error)
-          Vue.$toast.error(`[${code}] Failed to update user theme: ${message}`)
+          displayError(error, 'Failed to update theme')
           reject(error)
         })
     })
diff --git a/dbrepo-ui/components/search/AdvancedSearch.vue b/dbrepo-ui/components/search/AdvancedSearch.vue
index ff71d33cd3..3ed3e3cddc 100644
--- a/dbrepo-ui/components/search/AdvancedSearch.vue
+++ b/dbrepo-ui/components/search/AdvancedSearch.vue
@@ -13,6 +13,7 @@
               label="Type" />
           </v-col>
         </v-row>
+        <p>The following fields are <code>AND</code> connected and depend on the type above.</p>
         <v-row dense>
           <v-col cols="3">
             <v-text-field
@@ -69,6 +70,44 @@
               clearable />
           </v-col>
         </v-row>
+        <p v-if="isColumnFilter" class="mt-4">
+          If you select a <code>concept</code> and <code>unit</code>, you can search across columns regardless of their
+          unit of measurement.
+        </p>
+        <v-row v-if="isColumnFilter" dense>
+          <v-col cols="3">
+            <v-select
+              v-model="advancedSearchData['concept.uri']"
+              clearable
+              :items="concepts"
+              item-text="name"
+              item-value="uri"
+              label="Concept" />
+          </v-col>
+          <v-col cols="3">
+            <v-select
+              v-model="advancedSearchData['unit.uri']"
+              clearable
+              :items="units"
+              item-text="name"
+              item-value="uri"
+              label="Unit" />
+          </v-col>
+          <v-col cols="3">
+            <v-text-field
+              v-model="advancedSearchData['t1']"
+              clearable
+              type="number"
+              label="Start Value" />
+          </v-col>
+          <v-col cols="3">
+            <v-text-field
+              v-model="advancedSearchData['t2']"
+              clearable
+              type="number"
+              label="End Value" />
+          </v-col>
+        </v-row>
         <v-row dense>
           <v-btn class="mr-2" color="primary" :loading="loading" small @click="advancedSearch">
             Search
@@ -81,6 +120,8 @@
 <script>
 import SearchService from '@/api/search.service'
 import QueryMapper from '@/api/query.mapper'
+import SemanticService from '@/api/semantic.service'
+import SemanticMapper from '@/api/semantic.mapper'
 
 export default {
   data () {
@@ -88,6 +129,8 @@ export default {
       loading: false,
       loadingFields: false,
       showAdvancedSearch: false,
+      concepts: [],
+      units: [],
       columnTypes: QueryMapper.mySql8DataTypes().map((datatype) => {
         datatype.value = datatype.value.toUpperCase()
         return datatype
@@ -123,6 +166,9 @@ export default {
         hideNameField: selectedOption === 'identifier',
         hideInternalNameField: ['identifier', 'user', 'concept', 'unit'].includes(selectedOption)
       }
+    },
+    isColumnFilter () {
+      return this.advancedSearchData.type === 'column'
     }
   },
   watch: {
@@ -131,7 +177,6 @@ export default {
         if (!newType) {
           return
         }
-        console.debug('switched advanced search type to', newType)
         this.resetAdvancedSearchFields()
         this.loadingFields = true
         SearchService.getFields(newType)
@@ -149,6 +194,14 @@ export default {
   },
   mounted () {
     this.advancedSearch()
+    SemanticService.findAllConcepts()
+      .then((response) => {
+        this.concepts = SemanticMapper.mapConcepts(response)
+      })
+    SemanticService.findAllUnits()
+      .then((response) => {
+        this.units = SemanticMapper.mapUnits(response)
+      })
   },
   methods: {
     /* Removes all advanced search fields when switching the type */
@@ -164,6 +217,12 @@ export default {
       } else {
         delete this.advancedSearchData.search_term
       }
+      if ('t1' in this.advancedSearchData && this.advancedSearchData.t1) {
+        this.advancedSearchData.t1 = Number(this.advancedSearchData.t1)
+      }
+      if ('t2' in this.advancedSearchData && this.advancedSearchData.t2) {
+        this.advancedSearchData.t2 = Number(this.advancedSearchData.t2)
+      }
       this.loading = true
       SearchService.search(this.advancedSearchData)
         .then((response) => {
-- 
GitLab