diff --git a/modules/store/src/main/resources/db/migration/h2/V1.21.0__cast_function.sql b/modules/store/src/main/resources/db/migration/h2/V1.21.0__cast_function.sql new file mode 100644 index 00000000..146bf3d8 --- /dev/null +++ b/modules/store/src/main/resources/db/migration/h2/V1.21.0__cast_function.sql @@ -0,0 +1,12 @@ +DROP ALIAS IF EXISTS CAST_TO_NUMERIC; +CREATE ALIAS CAST_TO_NUMERIC AS ' +import java.text.*; +import java.math.*; +@CODE +BigDecimal castToNumeric(String s) throws Exception { + try { return new BigDecimal(s); } + catch (Exception e) { + return null; + } +} +' diff --git a/modules/store/src/main/resources/db/migration/mariadb/V1.21.0__cast_function.sql b/modules/store/src/main/resources/db/migration/mariadb/V1.21.0__cast_function.sql new file mode 100644 index 00000000..24ae76de --- /dev/null +++ b/modules/store/src/main/resources/db/migration/mariadb/V1.21.0__cast_function.sql @@ -0,0 +1,5 @@ +-- Create a function to cast to a numeric, if an error occurs return null +-- Could not get it working with decimal type, so using double +create or replace function CAST_TO_NUMERIC (s char(255)) +returns double deterministic +return cast(s as double); diff --git a/modules/store/src/main/resources/db/migration/postgresql/V1.21.0__cast_function.sql b/modules/store/src/main/resources/db/migration/postgresql/V1.21.0__cast_function.sql new file mode 100644 index 00000000..b603275c --- /dev/null +++ b/modules/store/src/main/resources/db/migration/postgresql/V1.21.0__cast_function.sql @@ -0,0 +1,9 @@ +-- Create a function to cast to a numeric, if an error occurs return null +create or replace function CAST_TO_NUMERIC(text) returns numeric as $$ +begin + return cast($1 as numeric); +exception + when invalid_text_representation then + return null; +end; +$$ language plpgsql immutable; diff --git a/modules/store/src/main/scala/docspell/store/qb/DBFunction.scala b/modules/store/src/main/scala/docspell/store/qb/DBFunction.scala index 58cca850..40db91b2 100644 --- a/modules/store/src/main/scala/docspell/store/qb/DBFunction.scala +++ b/modules/store/src/main/scala/docspell/store/qb/DBFunction.scala @@ -29,6 +29,8 @@ object DBFunction { case class Cast(expr: SelectExpr, newType: String) extends DBFunction + case class CastNumeric(expr: SelectExpr) extends DBFunction + case class Avg(expr: SelectExpr) extends DBFunction case class Sum(expr: SelectExpr) extends DBFunction diff --git a/modules/store/src/main/scala/docspell/store/qb/DSL.scala b/modules/store/src/main/scala/docspell/store/qb/DSL.scala index fba05543..63a0afc4 100644 --- a/modules/store/src/main/scala/docspell/store/qb/DSL.scala +++ b/modules/store/src/main/scala/docspell/store/qb/DSL.scala @@ -89,6 +89,9 @@ trait DSL extends DoobieMeta { def cast(expr: SelectExpr, targetType: String): DBFunction = DBFunction.Cast(expr, targetType) + def castNumeric(expr: SelectExpr): DBFunction = + DBFunction.CastNumeric(expr) + def coalesce(expr: SelectExpr, more: SelectExpr*): DBFunction.Coalesce = DBFunction.Coalesce(expr, more.toVector) diff --git a/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala b/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala index 18cdf909..71760463 100644 --- a/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala +++ b/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala @@ -242,16 +242,33 @@ object ItemQueryGenerator { )(coll: Ident, op: QOp, value: String): Select = { val cf = RCustomField.as("cf") val cfv = RCustomFieldValue.as("cfv") - val v = if (op == QOp.LowerLike) QueryWildcard.lower(value) else value - Select( - select(cfv.itemId), - from(cfv).innerJoin(cf, cf.id === cfv.field), - cf.cid === coll && sel(cf) && Condition.CompareVal( - cfv.value, - op, - v + + val baseSelect = + Select( + select(cfv.itemId), + from(cfv).innerJoin(cf, sel(cf) && cf.cid === coll && cf.id === cfv.field) ) - ) + + if (op == QOp.LowerLike) { + val v = QueryWildcard.lower(value) + baseSelect.where(Condition.CompareVal(cfv.value, op, v)) + } else { + val stringCmp = + Condition.CompareVal(cfv.value, op, value) + + value.toDoubleOption + .map { n => + val numericCmp = Condition.CompareFVal(castNumeric(cfv.value.s), op, n) + val fieldIsNumeric = + cf.ftype === CustomFieldType.Numeric || cf.ftype === CustomFieldType.Money + val fieldNotNumeric = + cf.ftype <> CustomFieldType.Numeric && cf.ftype <> CustomFieldType.Money + baseSelect.where( + (fieldIsNumeric && numericCmp) || (fieldNotNumeric && stringCmp) + ) + } + .getOrElse(baseSelect.where(stringCmp)) + } } } diff --git a/modules/store/src/main/scala/docspell/store/qb/impl/DBFunctionBuilder.scala b/modules/store/src/main/scala/docspell/store/qb/impl/DBFunctionBuilder.scala index 3a75569a..c57a9ac3 100644 --- a/modules/store/src/main/scala/docspell/store/qb/impl/DBFunctionBuilder.scala +++ b/modules/store/src/main/scala/docspell/store/qb/impl/DBFunctionBuilder.scala @@ -46,6 +46,9 @@ object DBFunctionBuilder extends CommonBuilder { fr" AS" ++ Fragment.const(newType) ++ sql")" + case DBFunction.CastNumeric(f) => + sql"CAST_TO_NUMERIC(" ++ SelectExprBuilder.build(f) ++ sql")" + case DBFunction.Avg(expr) => sql"AVG(" ++ SelectExprBuilder.build(expr) ++ fr")"