From 244d6050bd923544acfade353cc7e7f82b0e9703 Mon Sep 17 00:00:00 2001
From: Cezary Statkiewicz <cezary@baserow.io>
Date: Thu, 2 Jan 2025 09:19:28 +0000
Subject: [PATCH] #3112 number lookup field filters

---
 backend/src/baserow/contrib/database/apps.py  |  16 +
 .../contrib/database/fields/field_types.py    |  35 +-
 .../database/fields/filter_support/base.py    |  93 +--
 .../database/fields/filter_support/formula.py |  44 +-
 .../fields/filter_support/single_select.py    |  11 +-
 .../contrib/database/fields/registries.py     |  24 +
 .../django_expressions.py                     |  99 +++-
 .../database/formula/types/filter_support.py  |  91 +++
 .../database/formula/types/formula_type.py    |  11 +
 .../database/formula/types/formula_types.py   | 127 ++--
 .../database/views/array_view_filters.py      | 263 ++++++--
 .../contrib/database/views/view_filters.py    |  46 +-
 .../field/test_number_lookup_field_filters.py | 561 ++++++++++++++++++
 .../tests/baserow/contrib/database/utils.py   |   9 +-
 .../database/view/test_view_array_filters.py  |  33 +-
 .../3112_number_lookup_field_filters.json     |   7 +
 .../modules/database/arrayFilterMixins.js     | 119 +++-
 .../modules/database/arrayViewFilters.js      | 314 ++++++++--
 .../components/view/ViewFilterTypeNumber.vue  |   6 +
 web-frontend/modules/database/fieldTypes.js   | 106 ++--
 .../modules/database/formula/formulaTypes.js  |  35 +-
 web-frontend/modules/database/locales/en.json |  10 +-
 .../modules/database/mixins/numberField.js    |  12 +-
 web-frontend/modules/database/plugin.js       |  42 ++
 .../modules/database/utils/fieldFilters.js    |  69 ++-
 web-frontend/modules/database/utils/number.js |  33 +-
 .../database/arrayViewFiltersMatch.spec.js    | 350 ++++++++++-
 27 files changed, 2175 insertions(+), 391 deletions(-)
 create mode 100644 backend/src/baserow/contrib/database/formula/types/filter_support.py
 create mode 100644 backend/tests/baserow/contrib/database/field/test_number_lookup_field_filters.py
 create mode 100644 changelog/entries/unreleased/feature/3112_number_lookup_field_filters.json

diff --git a/backend/src/baserow/contrib/database/apps.py b/backend/src/baserow/contrib/database/apps.py
index c2329b112..29ce8c019 100755
--- a/backend/src/baserow/contrib/database/apps.py
+++ b/backend/src/baserow/contrib/database/apps.py
@@ -453,10 +453,18 @@ class DatabaseConfig(AppConfig):
             HasNotValueContainsViewFilterType,
             HasNotValueContainsWordViewFilterType,
             HasNotValueEqualViewFilterType,
+            HasNotValueHigherOrEqualTHanFilterType,
+            HasNotValueHigherThanFilterType,
+            HasNotValueLowerOrEqualTHanFilterType,
+            HasNotValueLowerThanFilterType,
             HasValueContainsViewFilterType,
             HasValueContainsWordViewFilterType,
             HasValueEqualViewFilterType,
+            HasValueHigherOrEqualThanFilter,
             HasValueLengthIsLowerThanViewFilterType,
+            HasValueLowerOrEqualThanFilter,
+            HasValueLowerThanFilter,
+            hasValueComparableToFilter,
         )
 
         view_filter_type_registry.register(HasValueEqualViewFilterType())
@@ -471,6 +479,14 @@ class DatabaseConfig(AppConfig):
         view_filter_type_registry.register(HasNotEmptyValueViewFilterType())
         view_filter_type_registry.register(HasAnySelectOptionEqualViewFilterType())
         view_filter_type_registry.register(HasNoneSelectOptionEqualViewFilterType())
+        view_filter_type_registry.register(HasValueLowerThanFilter())
+        view_filter_type_registry.register(HasValueLowerOrEqualThanFilter())
+        view_filter_type_registry.register(hasValueComparableToFilter())
+        view_filter_type_registry.register(HasValueHigherOrEqualThanFilter())
+        view_filter_type_registry.register(HasNotValueHigherOrEqualTHanFilterType())
+        view_filter_type_registry.register(HasNotValueHigherThanFilterType())
+        view_filter_type_registry.register(HasNotValueLowerOrEqualTHanFilterType())
+        view_filter_type_registry.register(HasNotValueLowerThanFilterType())
 
         from .views.view_aggregations import (
             AverageViewAggregationType,
diff --git a/backend/src/baserow/contrib/database/fields/field_types.py b/backend/src/baserow/contrib/database/fields/field_types.py
index 4e3487618..42f31ee4d 100755
--- a/backend/src/baserow/contrib/database/fields/field_types.py
+++ b/backend/src/baserow/contrib/database/fields/field_types.py
@@ -94,7 +94,7 @@ from baserow.contrib.database.api.views.errors import (
 from baserow.contrib.database.db.functions import RandomUUID
 from baserow.contrib.database.export_serialized import DatabaseExportSerializedStructure
 from baserow.contrib.database.fields.filter_support.formula import (
-    FormulaArrayFilterSupport,
+    FormulaFieldTypeArrayFilterSupport,
 )
 from baserow.contrib.database.formula import (
     BASEROW_FORMULA_TYPE_ALLOWED_FIELDS,
@@ -144,6 +144,7 @@ from baserow.core.user_files.handler import UserFileHandler
 from baserow.core.utils import list_to_comma_separated_string
 
 from .constants import (
+    BASEROW_BOOLEAN_FIELD_FALSE_VALUES,
     BASEROW_BOOLEAN_FIELD_TRUE_VALUES,
     UPSERT_OPTION_DICT_KEY,
     DeleteFieldStrategyEnum,
@@ -783,6 +784,21 @@ class NumberFieldType(FieldType):
             "number_separator": field.number_separator,
         }
 
+    def prepare_filter_value(self, field, model_field, value):
+        """
+        Verify if it's a valid and finite decimal value, but the filter value doesn't
+        need to respect the number_decimal_places, because they can change while the
+        filter_value remains the same.
+        """
+
+        try:
+            value = Decimal(value)
+            if not value.is_finite():
+                raise ValueError
+        except (InvalidOperation, ValueError, TypeError):
+            raise ValueError(f"Invalid value for number field: {value}")
+        return value
+
 
 class RatingFieldType(FieldType):
     type = "rating"
@@ -959,6 +975,14 @@ class BooleanFieldType(FieldType):
     ) -> BooleanField:
         return BooleanField()
 
+    def prepare_filter_value(self, field, model_field, value):
+        if value in BASEROW_BOOLEAN_FIELD_TRUE_VALUES:
+            return True
+        elif value in BASEROW_BOOLEAN_FIELD_FALSE_VALUES:
+            return False
+        else:
+            raise ValueError(f"Invalid value for boolean field: {value}")
+
 
 class DateFieldType(FieldType):
     type = "date"
@@ -4674,7 +4698,7 @@ class PhoneNumberFieldType(CollationSortMixin, CharFieldMatchingRegexFieldType):
         return collate_expression(Value(value))
 
 
-class FormulaFieldType(FormulaArrayFilterSupport, ReadOnlyFieldType):
+class FormulaFieldType(FormulaFieldTypeArrayFilterSupport, ReadOnlyFieldType):
     type = "formula"
     model_class = FormulaField
     _db_column_fields = []
@@ -5238,6 +5262,13 @@ class FormulaFieldType(FormulaArrayFilterSupport, ReadOnlyFieldType):
 
         return FormulaHandler.get_dependencies_field_names(serialized_field["formula"])
 
+    def prepare_filter_value(self, field, model_field, value):
+        (
+            field_instance,
+            field_type,
+        ) = self.get_field_instance_and_type_from_formula_field(field)
+        return field_type.prepare_filter_value(field_instance, model_field, value)
+
 
 class CountFieldType(FormulaFieldType):
     type = "count"
diff --git a/backend/src/baserow/contrib/database/fields/filter_support/base.py b/backend/src/baserow/contrib/database/fields/filter_support/base.py
index 60807d307..8b00e94bd 100644
--- a/backend/src/baserow/contrib/database/fields/filter_support/base.py
+++ b/backend/src/baserow/contrib/database/fields/filter_support/base.py
@@ -1,5 +1,5 @@
 import re
-import typing
+from typing import TYPE_CHECKING, Any, Dict, Type
 
 from django.contrib.postgres.fields import JSONField
 from django.db import models
@@ -13,17 +13,32 @@ from baserow.contrib.database.fields.field_filters import (
 )
 from baserow.contrib.database.formula.expression_generator.django_expressions import (
     BaserowFilterExpression,
+    ComparisonOperator,
     JSONArrayAllAreExpr,
+    JSONArrayCompareNumericValueExpr,
     JSONArrayContainsValueExpr,
     JSONArrayContainsValueLengthLowerThanExpr,
     JSONArrayContainsValueSimilarToExpr,
 )
 
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
     from baserow.contrib.database.fields.models import Field
 
 
 class HasValueEmptyFilterSupport:
+    def get_in_array_empty_value(self, field: "Field") -> Any:
+        """
+        Returns the value to use for filtering empty values in an array. An empty string
+        is used by default and works for test-like fields, but for number fields or
+        other types, this method should be overridden returning None or the most
+        appropriate value.
+
+        :param field: The related field's instance.
+        :return: The value to use for filtering empty values in an array.
+        """
+
+        return ""
+
     def get_in_array_empty_query(
         self, field_name: str, model_field: models.Field, field: "Field"
     ) -> OptionallyAnnotatedQ:
@@ -36,10 +51,13 @@ class HasValueEmptyFilterSupport:
         :return: A Q or AnnotatedQ filter given value.
         """
 
-        return Q(**{f"{field_name}__contains": Value([{"value": ""}], JSONField())})
+        empty_value = self.get_in_array_empty_value(field)
+        return Q(
+            **{f"{field_name}__contains": Value([{"value": empty_value}], JSONField())}
+        )
 
 
-class HasValueFilterSupport:
+class HasValueEqualFilterSupport:
     def get_in_array_is_query(
         self, field_name: str, value: str, model_field: models.Field, field: "Field"
     ) -> OptionallyAnnotatedQ:
@@ -53,9 +71,6 @@ class HasValueFilterSupport:
         :return: A Q or AnnotatedQ filter given value.
         """
 
-        if not value:
-            return Q()
-
         return Q(**{f"{field_name}__contains": Value([{"value": value}], JSONField())})
 
 
@@ -74,8 +89,6 @@ class HasValueContainsFilterSupport:
         :return: A Q or AnnotatedQ filter given value.
         """
 
-        if not value:
-            return Q()
         annotation_query = JSONArrayContainsValueExpr(
             F(field_name), Value(f"%{value}%"), output_field=BooleanField()
         )
@@ -103,9 +116,6 @@ class HasValueContainsWordFilterSupport:
         :return: A Q or AnnotatedQ filter given value.
         """
 
-        value = value.strip()
-        if not value:
-            return Q()
         value = re.escape(value.upper())
         annotation_query = JSONArrayContainsValueSimilarToExpr(
             F(field_name), Value(f"%\\m{value}\\M%"), output_field=BooleanField()
@@ -134,9 +144,6 @@ class HasValueLengthIsLowerThanFilterSupport:
         :return: A Q or AnnotatedQ filter given value.
         """
 
-        value = value.strip()
-        if not value:
-            return Q()
         try:
             converted_value = int(value)
         except (TypeError, ValueError):
@@ -182,29 +189,49 @@ class HasAllValuesEqualFilterSupport:
             return self.default_filter_on_exception()
 
 
-def get_array_json_filter_expression(
-    json_expression: typing.Type[BaserowFilterExpression], field_name: str, value: str
-) -> OptionallyAnnotatedQ:
-    """
-    helper to generate annotated query to get filtered json-based array.
-    `json_expression` should be a filter expression class.
+class HasNumericValueComparableToFilterSupport:
+    def get_has_numeric_value_comparable_to_filter_query(
+        self,
+        field_name: str,
+        value: str,
+        model_field: models.Field,
+        field: "Field",
+        comparison_op: ComparisonOperator,
+    ) -> OptionallyAnnotatedQ:
+        return get_array_json_filter_expression(
+            JSONArrayCompareNumericValueExpr,
+            field_name,
+            value,
+            comparison_op=comparison_op,
+        )
 
-    :param json_expression: BaserowFilterExpression to use
-    :param field_name: a name of a field
-    :param value: filter value
-    :param model_field:
-    :param field:
-    :return:
+
+def get_array_json_filter_expression(
+    json_expression: Type[BaserowFilterExpression],
+    field_name: str,
+    value: str,
+    **extra: Dict[str, Any],
+) -> AnnotatedQ:
+    """
+    Helper function to generate an AnnotatedQ for the given field and filtering
+    expression. This function ensure a consistent way to name the annotations so they
+    don't clash when combined with similar filters for different fields or values.
+
+
+    :param json_expression: BaserowFilterExpression to use for filtering.
+    :param field_name: the name of the field
+    :param value: filter the filter value.
+    :param extra: extra arguments for the json_expression.
+    :return: the annotated query for the filter.
     """
 
     annotation_query = json_expression(
-        F(field_name), Value(value), output_field=BooleanField()
+        F(field_name), Value(value), output_field=BooleanField(), **extra
     )
-    lookup_name = (json_expression.__name__).lower()
+    expr_name = json_expression.__name__.lower()
     hashed_value = hash(value)
+    annotation_name = f"{field_name}_{expr_name}_{hashed_value}"
     return AnnotatedQ(
-        annotation={
-            f"{field_name}_array_expr_{lookup_name}_{hashed_value}": annotation_query
-        },
-        q={f"{field_name}_array_expr_{lookup_name}_{hashed_value}": True},
+        annotation={annotation_name: annotation_query},
+        q={annotation_name: True},
     )
diff --git a/backend/src/baserow/contrib/database/fields/filter_support/formula.py b/backend/src/baserow/contrib/database/fields/filter_support/formula.py
index 5fe6deb9d..b29598545 100644
--- a/backend/src/baserow/contrib/database/fields/filter_support/formula.py
+++ b/backend/src/baserow/contrib/database/fields/filter_support/formula.py
@@ -4,28 +4,39 @@ from django.db import models
 from django.db.models import Q
 
 from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ
+from baserow.contrib.database.formula.expression_generator.django_expressions import (
+    ComparisonOperator,
+)
 
 from .base import (
     HasAllValuesEqualFilterSupport,
+    HasNumericValueComparableToFilterSupport,
     HasValueContainsFilterSupport,
     HasValueContainsWordFilterSupport,
     HasValueEmptyFilterSupport,
-    HasValueFilterSupport,
+    HasValueEqualFilterSupport,
     HasValueLengthIsLowerThanFilterSupport,
 )
 
 if typing.TYPE_CHECKING:
-    from baserow.contrib.database.fields.models import FormulaField
+    from baserow.contrib.database.fields.models import Field, FormulaField
 
 
-class FormulaArrayFilterSupport(
+class FormulaFieldTypeArrayFilterSupport(
     HasAllValuesEqualFilterSupport,
-    HasValueFilterSupport,
+    HasValueEqualFilterSupport,
     HasValueEmptyFilterSupport,
     HasValueContainsFilterSupport,
     HasValueContainsWordFilterSupport,
     HasValueLengthIsLowerThanFilterSupport,
+    HasNumericValueComparableToFilterSupport,
 ):
+    """
+    A mixin that acts as a proxy between the formula field and the specific array
+    formula function to call. Every method needs to be implemented here and forwarded
+    to the right array formula subtype method.
+    """
+
     def get_in_array_is_query(
         self,
         field_name: str,
@@ -42,6 +53,14 @@ class FormulaArrayFilterSupport(
             field_name, value, model_field, field_instance
         )
 
+    def get_in_array_empty_value(self, field: "Field") -> typing.Any:
+        (
+            field_instance,
+            field_type,
+        ) = self.get_field_instance_and_type_from_formula_field(field)
+
+        return field_type.get_in_array_empty_value(field_instance)
+
     def get_in_array_empty_query(self, field_name, model_field, field: "FormulaField"):
         (
             field_instance,
@@ -99,3 +118,20 @@ class FormulaArrayFilterSupport(
         return field_type.get_has_all_values_equal_query(
             field_name, value, model_field, field_instance
         )
+
+    def get_has_numeric_value_comparable_to_filter_query(
+        self,
+        field_name: str,
+        value: str,
+        model_field: models.Field,
+        field: "Field",
+        comparison_op: ComparisonOperator,
+    ) -> OptionallyAnnotatedQ:
+        (
+            field_instance,
+            field_type,
+        ) = self.get_field_instance_and_type_from_formula_field(field)
+
+        return field_type.get_has_numeric_value_comparable_to_filter_query(
+            field_name, value, model_field, field_instance, comparison_op
+        )
diff --git a/backend/src/baserow/contrib/database/fields/filter_support/single_select.py b/backend/src/baserow/contrib/database/fields/filter_support/single_select.py
index 0a158fd80..f08bc7759 100644
--- a/backend/src/baserow/contrib/database/fields/filter_support/single_select.py
+++ b/backend/src/baserow/contrib/database/fields/filter_support/single_select.py
@@ -1,7 +1,6 @@
 from functools import reduce
-from typing import TYPE_CHECKING, List
+from typing import TYPE_CHECKING, Any, List
 
-from django.contrib.postgres.fields import JSONField
 from django.db import models
 from django.db.models import BooleanField, F, Q, Value
 
@@ -19,7 +18,7 @@ from .base import (
     HasValueContainsFilterSupport,
     HasValueContainsWordFilterSupport,
     HasValueEmptyFilterSupport,
-    HasValueFilterSupport,
+    HasValueEqualFilterSupport,
 )
 
 if TYPE_CHECKING:
@@ -28,12 +27,12 @@ if TYPE_CHECKING:
 
 class SingleSelectFormulaTypeFilterSupport(
     HasValueEmptyFilterSupport,
-    HasValueFilterSupport,
+    HasValueEqualFilterSupport,
     HasValueContainsFilterSupport,
     HasValueContainsWordFilterSupport,
 ):
-    def get_in_array_empty_query(self, field_name, model_field, field: "Field"):
-        return Q(**{f"{field_name}__contains": Value([{"value": None}], JSONField())})
+    def get_in_array_empty_value(self, field: "Field") -> Any:
+        return None
 
     def get_in_array_is_query(
         self,
diff --git a/backend/src/baserow/contrib/database/fields/registries.py b/backend/src/baserow/contrib/database/fields/registries.py
index 52c00a0fa..25d9d744d 100644
--- a/backend/src/baserow/contrib/database/fields/registries.py
+++ b/backend/src/baserow/contrib/database/fields/registries.py
@@ -1825,6 +1825,30 @@ class FieldType(
 
         return value1 == value2
 
+    def prepare_filter_value(
+        self, field: "Field", model_field: django_models.Field, value: Any
+    ) -> Any:
+        """
+        Prepare a non-empty value string to be used in a view filter, verifying if it is
+        compatible with the given field and model_field. This method must be called
+        before the value is passed to the filter method that requires it. This is useful
+        to ensure that comparisons are done correctly and that the value is correctly
+        prepared for the database, like converting a string to a date object or a
+        number.
+
+        :param field: The field instance that the value belongs to.
+        :param model_field: The model field that the value must be prepared for.
+        :param value: The value that must be prepared for filtering.
+        :return: The prepared value.
+        :raises ValueError: If the value is not compatible for the given field and
+            model_field.
+        """
+
+        try:
+            return model_field.get_prep_value(value)
+        except ValidationError as e:
+            raise ValueError(str(e))
+
 
 class ReadOnlyFieldType(FieldType):
     read_only = True
diff --git a/backend/src/baserow/contrib/database/formula/expression_generator/django_expressions.py b/backend/src/baserow/contrib/database/formula/expression_generator/django_expressions.py
index 0e79f257c..0b4ba4cb5 100644
--- a/backend/src/baserow/contrib/database/formula/expression_generator/django_expressions.py
+++ b/backend/src/baserow/contrib/database/formula/expression_generator/django_expressions.py
@@ -1,4 +1,5 @@
 import typing
+from enum import Enum
 
 from django.contrib.postgres.aggregates.mixins import OrderableAggMixin
 from django.db import NotSupportedError
@@ -148,15 +149,33 @@ class BaserowFilterExpression(Expression):
 
         return c
 
-    def as_sql(self, compiler, connection, template=None):
-        sql_value, params_value = compiler.compile(self.value)
-
-        template = template or self.template
-        data = {
+    def get_template_data(self, sql_value) -> dict:
+        return {
             "field_name": f'"{self.field_name.field.column}"',
             "value": sql_value,
         }
-        return template % data, params_value
+
+    def render_template_as_sql(
+        self, filter_value: str, template: str | None = None
+    ) -> str:
+        """
+        Renders the template with the given sql_value and returns the result. If a
+        custom template is provided, it will be used instead of the default one.
+
+        :param filter_value: The value that will be used in the template.
+        :param template: The custom template to use. If not provided, the default one
+            will be used.
+        :return: The rendered template with data that will be used as SQL.
+        """
+
+        template = template or self.template
+        data = self.get_template_data(filter_value)
+        return template % data
+
+    def as_sql(self, compiler, connection, template=None):
+        sql_value, sql_params = compiler.compile(self.value)
+        sql_query = self.render_template_as_sql(sql_value, template)
+        return sql_query, sql_params
 
 
 class FileNameContainsExpr(BaserowFilterExpression):
@@ -164,7 +183,7 @@ class FileNameContainsExpr(BaserowFilterExpression):
     template = (
         f"""
         EXISTS(
-            SELECT attached_files ->> 'visible_name'
+            SELECT 1
             FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as attached_files
             WHERE UPPER(attached_files ->> 'visible_name') LIKE UPPER(%(value)s)
         )
@@ -178,7 +197,7 @@ class JSONArrayContainsValueExpr(BaserowFilterExpression):
     template = (
         f"""
         EXISTS(
-            SELECT filtered_field ->> 'value'
+            SELECT 1
             FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
             WHERE UPPER(filtered_field ->> 'value') LIKE UPPER(%(value)s::text)
         )
@@ -192,7 +211,7 @@ class JSONArrayContainsValueSimilarToExpr(BaserowFilterExpression):
     template = (
         f"""
         EXISTS(
-            SELECT filtered_field ->> 'value'
+            SELECT 1
             FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
             WHERE UPPER(filtered_field ->> 'value') SIMILAR TO %(value)s
         )
@@ -206,7 +225,7 @@ class JSONArrayContainsValueLengthLowerThanExpr(BaserowFilterExpression):
     template = (
         f"""
         EXISTS(
-            SELECT filtered_field ->> 'value'
+            SELECT 1
             FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
             WHERE LENGTH(filtered_field ->> 'value') < %(value)s
         )
@@ -219,7 +238,7 @@ class JSONArrayAllAreExpr(BaserowFilterExpression):
     # fmt: off
     template = (
         f"""
-        upper(%(value)s::text) =ALL(
+        upper(%(value)s::text) = ALL(
             SELECT upper(filtered_field ->> 'value')
             FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
         ) AND JSONB_ARRAY_LENGTH(%(field_name)s) > 0
@@ -233,7 +252,7 @@ class JSONArrayEqualSelectOptionIdExpr(BaserowFilterExpression):
     template = (
         f"""
         EXISTS(
-            SELECT filtered_field -> 'value' ->> 'id'
+            SELECT 1
             FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
             WHERE (filtered_field -> 'value' ->> 'id') LIKE (%(value)s)
         )
@@ -247,7 +266,7 @@ class JSONArrayContainsSelectOptionValueExpr(BaserowFilterExpression):
     template = (
         f"""
         EXISTS(
-            SELECT filtered_field -> 'value' ->> 'value'
+            SELECT 1
             FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
             WHERE UPPER(filtered_field -> 'value' ->> 'value') LIKE UPPER(%(value)s)
         )
@@ -261,10 +280,62 @@ class JSONArrayContainsSelectOptionValueSimilarToExpr(BaserowFilterExpression):
     template = (
         r"""
         EXISTS(
-            SELECT filtered_field -> 'value' ->> 'value'
+            SELECT 1
             FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
             WHERE filtered_field -> 'value' ->> 'value' ~* ('\y' || %(value)s || '\y')
         )
         """  # nosec B608 %(value)s
     )
     # fmt: on
+
+
+class ComparisonOperator(Enum):
+    """
+    An enumeration of the comparison operators that can be used to compare a number
+    field value.
+    """
+
+    EQUAL = "="
+    LOWER_THAN = "<"
+    LOWER_THAN_OR_EQUAL = "<="
+    HIGHER_THAN = ">"
+    HIGHER_THAN_OR_EQUAL = ">="
+
+
+class JSONArrayCompareNumericValueExpr(BaserowFilterExpression):
+    """
+    Base class for expressions that compare a numeric value in a JSON array.
+    Together with the field_name and value, a comparison operator must be provided to be
+    used in the template.
+    """
+
+    def __init__(
+        self,
+        field_name: F,
+        value: Value,
+        comparison_op: ComparisonOperator,
+        output_field: Field,
+    ):
+        super().__init__(field_name, value, output_field)
+        if not isinstance(comparison_op, ComparisonOperator):
+            raise ValueError(
+                f"comparison_op must be a ComparisonOperator, not {type(comparison_op)}"
+            )
+        self.comparison_op = comparison_op
+
+    # fmt: off
+    template = (
+        f"""
+            EXISTS(
+                SELECT 1
+                FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
+                WHERE (filtered_field ->> 'value')::numeric %(comparison_op)s %(value)s::numeric
+            )
+            """  # nosec B608 %(value)s %(comparison_op)s
+    )
+    # fmt: on
+
+    def get_template_data(self, sql_value) -> dict:
+        data = super().get_template_data(sql_value)
+        data["comparison_op"] = self.comparison_op.value
+        return data
diff --git a/backend/src/baserow/contrib/database/formula/types/filter_support.py b/backend/src/baserow/contrib/database/formula/types/filter_support.py
new file mode 100644
index 000000000..815a36d30
--- /dev/null
+++ b/backend/src/baserow/contrib/database/formula/types/filter_support.py
@@ -0,0 +1,91 @@
+import typing
+
+from django.db import models
+
+from baserow.contrib.database.fields.filter_support.base import (
+    HasAllValuesEqualFilterSupport,
+    HasNumericValueComparableToFilterSupport,
+    HasValueContainsFilterSupport,
+    HasValueContainsWordFilterSupport,
+    HasValueEmptyFilterSupport,
+    HasValueEqualFilterSupport,
+    HasValueLengthIsLowerThanFilterSupport,
+)
+from baserow.contrib.database.formula.expression_generator.django_expressions import (
+    ComparisonOperator,
+)
+
+if typing.TYPE_CHECKING:
+    from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ
+    from baserow.contrib.database.fields.models import Field
+
+
+class BaserowFormulaArrayFilterSupportMixin(
+    HasAllValuesEqualFilterSupport,
+    HasValueEmptyFilterSupport,
+    HasValueEqualFilterSupport,
+    HasValueContainsFilterSupport,
+    HasValueContainsWordFilterSupport,
+    HasValueLengthIsLowerThanFilterSupport,
+    HasNumericValueComparableToFilterSupport,
+):
+    """
+    This mixin proxies all the array formula filters methods to the formula subtype.
+    """
+
+    def get_in_array_empty_value(self, field):
+        field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
+        return self.sub_type.get_in_array_empty_value(field_instance)
+
+    def get_in_array_empty_query(self, field_name, model_field, field):
+        field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
+        return self.sub_type.get_in_array_empty_query(
+            field_name, model_field, field_instance
+        )
+
+    def get_in_array_is_query(self, field_name, value, model_field, field):
+        field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
+        return self.sub_type.get_in_array_is_query(
+            field_name, value, model_field, field_instance
+        )
+
+    def get_in_array_contains_query(self, field_name, value, model_field, field):
+        field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
+        return self.sub_type.get_in_array_contains_query(
+            field_name, value, model_field, field_instance
+        )
+
+    def get_in_array_contains_word_query(self, field_name, value, model_field, field):
+        field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
+        return self.sub_type.get_in_array_contains_word_query(
+            field_name, value, model_field, field_instance
+        )
+
+    def get_in_array_length_is_lower_than_query(
+        self, field_name, value, model_field, field
+    ):
+        field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
+        return self.sub_type.get_in_array_length_is_lower_than_query(
+            field_name, value, model_field, field_instance
+        )
+
+    def get_has_all_values_equal_query(
+        self, field_name: str, value: str, model_field: models.Field, field: "Field"
+    ) -> "OptionallyAnnotatedQ":
+        field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
+        return self.sub_type.get_has_all_values_equal_query(
+            field_name, value, model_field, field_instance
+        )
+
+    def get_has_numeric_value_comparable_to_filter_query(
+        self,
+        field_name: str,
+        value: str,
+        model_field: models.Field,
+        field: "Field",
+        comparison_op: ComparisonOperator,
+    ) -> "OptionallyAnnotatedQ":
+        field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
+        return self.sub_type.get_has_numeric_value_comparable_to_filter_query(
+            field_name, value, model_field, field_instance, comparison_op
+        )
diff --git a/backend/src/baserow/contrib/database/formula/types/formula_type.py b/backend/src/baserow/contrib/database/formula/types/formula_type.py
index 722fdde9c..6e6d2b0e7 100644
--- a/backend/src/baserow/contrib/database/formula/types/formula_type.py
+++ b/backend/src/baserow/contrib/database/formula/types/formula_type.py
@@ -497,6 +497,17 @@ class BaserowFormulaType(abc.ABC):
         field_instance.id = field.id
         return field_type.is_searchable(field_instance)
 
+    def prepare_filter_value(self, field, model_field, value):
+        """
+        Use the Baserow field type method where possible to prepare the filter value.
+        """
+
+        (
+            field_instance,
+            field_type,
+        ) = self.get_baserow_field_instance_and_type()
+        return field_type.prepare_filter_value(field_instance, model_field, value)
+
 
 class BaserowFormulaInvalidType(BaserowFormulaType):
     is_valid = False
diff --git a/backend/src/baserow/contrib/database/formula/types/formula_types.py b/backend/src/baserow/contrib/database/formula/types/formula_types.py
index 31b6a3326..f0d136b8f 100644
--- a/backend/src/baserow/contrib/database/formula/types/formula_types.py
+++ b/backend/src/baserow/contrib/database/formula/types/formula_types.py
@@ -6,7 +6,6 @@ from typing import Any, List, Optional, Set, Type, Union
 
 from django.contrib.postgres.expressions import ArraySubquery
 from django.contrib.postgres.fields import ArrayField, JSONField
-from django.core.exceptions import ValidationError
 from django.db import models
 from django.db.models import (
     Expression,
@@ -21,7 +20,6 @@ from django.db.models import (
 from django.db.models.functions import Cast, Concat
 
 from dateutil import parser
-from loguru import logger
 from rest_framework import serializers
 from rest_framework.fields import Field
 
@@ -37,16 +35,14 @@ from baserow.contrib.database.fields.field_filters import (
 from baserow.contrib.database.fields.field_sortings import OptionallyAnnotatedOrderBy
 from baserow.contrib.database.fields.filter_support.base import (
     HasAllValuesEqualFilterSupport,
+    HasNumericValueComparableToFilterSupport,
     HasValueContainsFilterSupport,
     HasValueContainsWordFilterSupport,
     HasValueEmptyFilterSupport,
-    HasValueFilterSupport,
+    HasValueEqualFilterSupport,
     HasValueLengthIsLowerThanFilterSupport,
     get_array_json_filter_expression,
 )
-from baserow.contrib.database.fields.filter_support.exceptions import (
-    FilterNotSupportedException,
-)
 from baserow.contrib.database.fields.filter_support.single_select import (
     SingleSelectFormulaTypeFilterSupport,
 )
@@ -65,10 +61,15 @@ from baserow.contrib.database.formula.ast.tree import (
     BaserowStringLiteral,
 )
 from baserow.contrib.database.formula.expression_generator.django_expressions import (
+    ComparisonOperator,
+    JSONArrayCompareNumericValueExpr,
     JSONArrayContainsValueExpr,
 )
 from baserow.contrib.database.formula.registries import formula_function_registry
 from baserow.contrib.database.formula.types.exceptions import UnknownFormulaType
+from baserow.contrib.database.formula.types.filter_support import (
+    BaserowFormulaArrayFilterSupportMixin,
+)
 from baserow.contrib.database.formula.types.formula_type import (
     BaserowFormulaInvalidType,
     BaserowFormulaType,
@@ -81,7 +82,14 @@ from baserow.core.utils import list_to_comma_separated_string
 
 
 class BaserowJSONBObjectBaseType(BaserowFormulaValidType, ABC):
-    pass
+    def prepare_filter_value(self, field, model_field, value):
+        """
+        Since the subclasses don't have a baserow_field_type or data might be stored
+        differently due to the JSONB nature, return the value as is here and let the
+        filter method handle it.
+        """
+
+        return value
 
 
 class BaserowFormulaBaseTextType(BaserowFormulaTypeHasEmptyBaserowExpression):
@@ -130,7 +138,7 @@ class BaserowFormulaBaseTextType(BaserowFormulaTypeHasEmptyBaserowExpression):
 
 class BaserowFormulaTextType(
     HasValueEmptyFilterSupport,
-    HasValueFilterSupport,
+    HasValueEqualFilterSupport,
     HasValueContainsFilterSupport,
     HasValueContainsWordFilterSupport,
     HasValueLengthIsLowerThanFilterSupport,
@@ -328,7 +336,12 @@ class BaserowFormulaButtonType(BaserowFormulaLinkType):
 
 
 class BaserowFormulaNumberType(
-    BaserowFormulaTypeHasEmptyBaserowExpression, BaserowFormulaValidType
+    HasValueEmptyFilterSupport,
+    HasValueEqualFilterSupport,
+    HasValueContainsFilterSupport,
+    HasNumericValueComparableToFilterSupport,
+    BaserowFormulaTypeHasEmptyBaserowExpression,
+    BaserowFormulaValidType,
 ):
     type = "number"
     baserow_field_type = "number"
@@ -460,13 +473,26 @@ class BaserowFormulaNumberType(
             ),
         )
 
+    def get_in_array_empty_value(self, field: "Field") -> Any:
+        return None
+
+    def get_in_array_is_query(
+        self, field_name: str, value: str, model_field: models.Field, field: "Field"
+    ) -> OptionallyAnnotatedQ:
+        return get_array_json_filter_expression(
+            JSONArrayCompareNumericValueExpr,
+            field_name,
+            value,
+            comparison_op=ComparisonOperator.EQUAL,
+        )
+
     def __str__(self) -> str:
         return f"number({self.number_decimal_places})"
 
 
 class BaserowFormulaBooleanType(
     HasAllValuesEqualFilterSupport,
-    HasValueFilterSupport,
+    HasValueEqualFilterSupport,
     BaserowFormulaTypeHasEmptyBaserowExpression,
     BaserowFormulaValidType,
 ):
@@ -500,22 +526,9 @@ class BaserowFormulaBooleanType(
     ):
         return expr
 
-    def _get_prep_value(self, value: str):
-        from baserow.contrib.database.fields.registries import field_type_registry
-
-        baserow_field_type = field_type_registry.get(self.baserow_field_type)
-        # boolean field type doesn't expect instance value
-        field_instance = baserow_field_type.get_model_field(None)
-        try:
-            # get_prep_value can return None
-            return field_instance.get_prep_value(value) or False
-        except ValidationError:
-            return False
-
     def get_in_array_is_query(
         self, field_name: str, value: str, model_field: models.Field, field: "Field"
     ) -> OptionallyAnnotatedQ:
-        value = self._get_prep_value(value)
         return get_array_json_filter_expression(
             JSONArrayContainsValueExpr, field_name, value
         )
@@ -533,7 +546,6 @@ class BaserowFormulaBooleanType(
     def get_has_all_values_equal_query(
         self, field_name: str, value: str, model_field: models.Field, field: "Field"
     ) -> "OptionallyAnnotatedQ":
-        value = self._get_prep_value(value)
         return super().get_has_all_values_equal_query(
             field_name, value, model_field, field
         )
@@ -1062,12 +1074,7 @@ class BaserowFormulaSingleFileType(BaserowJSONBObjectBaseType):
 
 
 class BaserowFormulaArrayType(
-    HasAllValuesEqualFilterSupport,
-    HasValueEmptyFilterSupport,
-    HasValueFilterSupport,
-    HasValueContainsFilterSupport,
-    HasValueContainsWordFilterSupport,
-    HasValueLengthIsLowerThanFilterSupport,
+    BaserowFormulaArrayFilterSupportMixin,
     BaserowFormulaValidType,
 ):
     type = "array"
@@ -1235,57 +1242,6 @@ class BaserowFormulaArrayType(
     def contains_query(self, field_name, value, model_field, field):
         return Q()
 
-    def get_in_array_is_query(self, field_name, value, model_field, field):
-        if not isinstance(self.sub_type, HasValueFilterSupport):
-            raise FilterNotSupportedException()
-        return self.sub_type.get_in_array_is_query(
-            field_name, value, model_field, field
-        )
-
-    def get_in_array_empty_query(self, field_name, model_field, field):
-        if not isinstance(self.sub_type, HasValueEmptyFilterSupport):
-            raise FilterNotSupportedException()
-
-        return self.sub_type.get_in_array_empty_query(field_name, model_field, field)
-
-    def get_in_array_contains_query(self, field_name, value, model_field, field):
-        if not isinstance(self.sub_type, HasValueContainsFilterSupport):
-            raise FilterNotSupportedException()
-
-        return self.sub_type.get_in_array_contains_query(
-            field_name, value, model_field, field
-        )
-
-    def get_in_array_contains_word_query(self, field_name, value, model_field, field):
-        if not isinstance(self.sub_type, HasValueContainsWordFilterSupport):
-            raise FilterNotSupportedException()
-
-        return self.sub_type.get_in_array_contains_word_query(
-            field_name, value, model_field, field
-        )
-
-    def get_in_array_length_is_lower_than_query(
-        self, field_name, value, model_field, field
-    ):
-        if not isinstance(self.sub_type, HasValueLengthIsLowerThanFilterSupport):
-            raise FilterNotSupportedException()
-
-        return self.sub_type.get_in_array_length_is_lower_than_query(
-            field_name, value, model_field, field
-        )
-
-    def get_has_all_values_equal_query(
-        self, field_name: str, value: str, model_field: models.Field, field: "Field"
-    ) -> "OptionallyAnnotatedQ":
-        if not isinstance(self.sub_type, HasAllValuesEqualFilterSupport):
-            logger.warning(
-                f"field {field} is not from {HasAllValuesEqualFilterSupport} hierarchy"
-            )
-            raise FilterNotSupportedException()
-        return self.sub_type.get_has_all_values_equal_query(
-            field_name, value, model_field, field
-        )
-
     def get_alter_column_prepare_old_value(self, connection, from_field, to_field):
         return "p_in = '';"
 
@@ -1362,10 +1318,8 @@ class BaserowFormulaArrayType(
         return None
 
     def check_if_compatible_with(self, compatible_formula_types: List[str]):
-        return (
-            self.type in compatible_formula_types
-            or str(self) in compatible_formula_types
-        )
+        self_as_str = self.formula_array_type_as_str(self.sub_type.type)
+        return self_as_str in compatible_formula_types
 
     def __str__(self) -> str:
         return self.formula_array_type_as_str(self.sub_type)
@@ -1398,6 +1352,9 @@ class BaserowFormulaArrayType(
             )
         }
 
+    def prepare_filter_value(self, field, model_field, value):
+        return self.sub_type.prepare_filter_value(field, model_field, value)
+
 
 class BaserowFormulaSingleSelectType(
     SingleSelectFormulaTypeFilterSupport,
diff --git a/backend/src/baserow/contrib/database/views/array_view_filters.py b/backend/src/baserow/contrib/database/views/array_view_filters.py
index b61548d0a..e6a47beab 100644
--- a/backend/src/baserow/contrib/database/views/array_view_filters.py
+++ b/backend/src/baserow/contrib/database/views/array_view_filters.py
@@ -1,18 +1,25 @@
+from abc import ABC, abstractmethod
+
+from django.db.models import Q
+
 from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ
 from baserow.contrib.database.fields.field_types import FormulaFieldType
 from baserow.contrib.database.fields.filter_support.base import (
-    HasAllValuesEqualFilterSupport,
+    HasNumericValueComparableToFilterSupport,
     HasValueContainsFilterSupport,
     HasValueContainsWordFilterSupport,
     HasValueEmptyFilterSupport,
-    HasValueFilterSupport,
+    HasValueEqualFilterSupport,
     HasValueLengthIsLowerThanFilterSupport,
 )
-from baserow.contrib.database.fields.filter_support.exceptions import (
-    FilterNotSupportedException,
-)
 from baserow.contrib.database.fields.registries import field_type_registry
-from baserow.contrib.database.formula import BaserowFormulaTextType
+from baserow.contrib.database.formula import (
+    BaserowFormulaNumberType,
+    BaserowFormulaTextType,
+)
+from baserow.contrib.database.formula.expression_generator.django_expressions import (
+    ComparisonOperator,
+)
 from baserow.contrib.database.formula.types.formula_types import (
     BaserowFormulaBooleanType,
     BaserowFormulaCharType,
@@ -37,18 +44,13 @@ class HasEmptyValueViewFilterType(ViewFilterType):
             FormulaFieldType.array_of(BaserowFormulaCharType.type),
             FormulaFieldType.array_of(BaserowFormulaURLType.type),
             FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
+            FormulaFieldType.array_of(BaserowFormulaNumberType.type),
         ),
     ]
 
     def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
-        field_type = field_type_registry.get_by_model(field)
-        try:
-            if not isinstance(field_type, HasValueEmptyFilterSupport):
-                raise FilterNotSupportedException(field_type)
-
-            return field_type.get_in_array_empty_query(field_name, model_field, field)
-        except FilterNotSupportedException:
-            return self.default_filter_on_exception()
+        field_type: HasValueEmptyFilterSupport = field_type_registry.get_by_model(field)
+        return field_type.get_in_array_empty_query(field_name, model_field, field)
 
 
 class HasNotEmptyValueViewFilterType(
@@ -57,7 +59,43 @@ class HasNotEmptyValueViewFilterType(
     type = "has_not_empty_value"
 
 
-class HasValueEqualViewFilterType(ViewFilterType):
+class ComparisonHasValueFilter(ViewFilterType, ABC):
+    """
+    A filter that can be used to compare the values in an array with a specific value
+    using a comparison operator.
+    """
+
+    def get_filter(
+        self, field_name, value: str, model_field, field
+    ) -> OptionallyAnnotatedQ:
+        if value == "":
+            return Q()
+
+        field_type = field_type_registry.get_by_model(field)
+        try:
+            filter_value = field_type.prepare_filter_value(field, model_field, value)
+        except ValueError:  # invalid filter value for the field
+            return self.default_filter_on_exception()
+
+        return self.get_filter_expression(field_name, filter_value, model_field, field)
+
+    @abstractmethod
+    def get_filter_expression(
+        self, field_name, value, model_field, field
+    ) -> OptionallyAnnotatedQ:
+        """
+        Return the filter expression to use for the required comparison.
+
+        :param field_name: The name of the field to filter on.
+        :param value: A non-empty string value to compare against.
+        :param model_field: The model field instance.
+        :param field: The field instance.
+        :return: The filter expression to use.
+        :raises ValidationError: If the value cannot be parsed to the proper type.
+        """
+
+
+class HasValueEqualViewFilterType(ComparisonHasValueFilter):
     """
     The filter can be used to check for "is" condition for
     items in an array.
@@ -71,19 +109,15 @@ class HasValueEqualViewFilterType(ViewFilterType):
             FormulaFieldType.array_of(BaserowFormulaURLType.type),
             FormulaFieldType.array_of(BaserowFormulaBooleanType.type),
             FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
+            FormulaFieldType.array_of(BaserowFormulaNumberType.type),
         ),
     ]
 
-    def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
-        field_type = field_type_registry.get_by_model(field)
-        try:
-            if not isinstance(field_type, HasValueFilterSupport):
-                raise FilterNotSupportedException(field_type)
-            return field_type.get_in_array_is_query(
-                field_name, value, model_field, field
-            )
-        except FilterNotSupportedException:
-            return self.default_filter_on_exception()
+    def get_filter_expression(
+        self, field_name, value, model_field, field
+    ) -> OptionallyAnnotatedQ:
+        field_type: HasValueEqualFilterSupport = field_type_registry.get_by_model(field)
+        return field_type.get_in_array_is_query(field_name, value, model_field, field)
 
 
 class HasNotValueEqualViewFilterType(
@@ -105,20 +139,20 @@ class HasValueContainsViewFilterType(ViewFilterType):
             FormulaFieldType.array_of(BaserowFormulaCharType.type),
             FormulaFieldType.array_of(BaserowFormulaURLType.type),
             FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
+            FormulaFieldType.array_of(BaserowFormulaNumberType.type),
         ),
     ]
 
     def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
-        field_type = field_type_registry.get_by_model(field)
-        try:
-            if not isinstance(field_type, HasValueContainsFilterSupport):
-                raise FilterNotSupportedException(field_type)
+        if value == "":
+            return Q()
 
-            return field_type.get_in_array_contains_query(
-                field_name, value, model_field, field
-            )
-        except FilterNotSupportedException:
-            return self.default_filter_on_exception()
+        field_type: HasValueContainsFilterSupport = field_type_registry.get_by_model(
+            field
+        )
+        return field_type.get_in_array_contains_query(
+            field_name, value, model_field, field
+        )
 
 
 class HasNotValueContainsViewFilterType(
@@ -144,16 +178,15 @@ class HasValueContainsWordViewFilterType(ViewFilterType):
     ]
 
     def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
-        field_type = field_type_registry.get_by_model(field)
-        try:
-            if not isinstance(field_type, HasValueContainsWordFilterSupport):
-                raise FilterNotSupportedException(field_type)
+        if value == "":
+            return Q()
 
-            return field_type.get_in_array_contains_word_query(
-                field_name, value, model_field, field
-            )
-        except FilterNotSupportedException:
-            return self.default_filter_on_exception()
+        field_type: HasValueContainsWordFilterSupport = (
+            field_type_registry.get_by_model(field)
+        )
+        return field_type.get_in_array_contains_word_query(
+            field_name, value, model_field, field
+        )
 
 
 class HasNotValueContainsWordViewFilterType(
@@ -178,19 +211,25 @@ class HasValueLengthIsLowerThanViewFilterType(ViewFilterType):
     ]
 
     def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
-        field_type = field_type_registry.get_by_model(field)
-        try:
-            if not isinstance(field_type, HasValueLengthIsLowerThanFilterSupport):
-                raise FilterNotSupportedException(field_type)
+        value = value.strip()
+        if value == "":
+            return Q()
 
-            return field_type.get_in_array_length_is_lower_than_query(
-                field_name, value, model_field, field
-            )
-        except FilterNotSupportedException:
+        try:
+            # The value is expected to be an integer representing the length to compare
+            filter_value = int(value)
+        except (ValueError, TypeError):
             return self.default_filter_on_exception()
 
+        field_type: HasValueLengthIsLowerThanFilterSupport = (
+            field_type_registry.get_by_model(field)
+        )
+        return field_type.get_in_array_length_is_lower_than_query(
+            field_name, filter_value, model_field, field
+        )
 
-class HasAllValuesEqualViewFilterType(ViewFilterType):
+
+class HasAllValuesEqualViewFilterType(ComparisonHasValueFilter):
     """
     The filter checks if all values in an array are equal to a specific value.
     """
@@ -202,16 +241,15 @@ class HasAllValuesEqualViewFilterType(ViewFilterType):
         ),
     ]
 
-    def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
-        try:
-            field_type = field_type_registry.get_by_model(field)
-            if not isinstance(field_type, HasAllValuesEqualFilterSupport):
-                raise FilterNotSupportedException(field_type)
-            return field_type.get_has_all_values_equal_query(
-                field_name, value, model_field, field
-            )
-        except FilterNotSupportedException:
-            return self.default_filter_on_exception()
+    def get_filter_expression(
+        self, field_name, value, model_field, field
+    ) -> OptionallyAnnotatedQ:
+        field_type: HasAllValuesEqualViewFilterType = field_type_registry.get_by_model(
+            field
+        )
+        return field_type.get_has_all_values_equal_query(
+            field_name, value, model_field, field
+        )
 
 
 class HasAnySelectOptionEqualViewFilterType(HasValueEqualViewFilterType):
@@ -228,6 +266,9 @@ class HasAnySelectOptionEqualViewFilterType(HasValueEqualViewFilterType):
     ]
 
     def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
+        if value == "":
+            return Q()
+
         return super().get_filter(field_name, value.split(","), model_field, field)
 
 
@@ -240,3 +281,101 @@ class HasNoneSelectOptionEqualViewFilterType(
     """
 
     type = "has_none_select_option_equal"
+
+
+class hasValueComparableToFilter(ComparisonHasValueFilter):
+    type = "has_value_higher"
+    compatible_field_types = [
+        FormulaFieldType.compatible_with_formula_types(
+            FormulaFieldType.array_of(BaserowFormulaNumberType.type)
+        ),
+    ]
+
+    def get_filter_expression(self, field_name, value, model_field, field):
+        field_type: HasNumericValueComparableToFilterSupport = (
+            field_type_registry.get_by_model(field)
+        )
+        return field_type.get_has_numeric_value_comparable_to_filter_query(
+            field_name, value, model_field, field, ComparisonOperator.HIGHER_THAN
+        )
+
+
+class HasNotValueHigherThanFilterType(
+    NotViewFilterTypeMixin, hasValueComparableToFilter
+):
+    type = "has_not_value_higher"
+
+
+class HasValueHigherOrEqualThanFilter(ComparisonHasValueFilter):
+    type = "has_value_higher_or_equal"
+    compatible_field_types = [
+        FormulaFieldType.compatible_with_formula_types(
+            FormulaFieldType.array_of(BaserowFormulaNumberType.type)
+        ),
+    ]
+
+    def get_filter_expression(self, field_name, value, model_field, field):
+        field_type: HasNumericValueComparableToFilterSupport = (
+            field_type_registry.get_by_model(field)
+        )
+        return field_type.get_has_numeric_value_comparable_to_filter_query(
+            field_name,
+            value,
+            model_field,
+            field,
+            ComparisonOperator.HIGHER_THAN_OR_EQUAL,
+        )
+
+
+class HasNotValueHigherOrEqualTHanFilterType(
+    NotViewFilterTypeMixin, HasValueHigherOrEqualThanFilter
+):
+    type = "has_not_value_higher_or_equal"
+
+
+class HasValueLowerThanFilter(ComparisonHasValueFilter):
+    type = "has_value_lower"
+    compatible_field_types = [
+        FormulaFieldType.compatible_with_formula_types(
+            FormulaFieldType.array_of(BaserowFormulaNumberType.type)
+        ),
+    ]
+
+    def get_filter_expression(self, field_name, value, model_field, field):
+        field_type: HasNumericValueComparableToFilterSupport = (
+            field_type_registry.get_by_model(field)
+        )
+        return field_type.get_has_numeric_value_comparable_to_filter_query(
+            field_name, value, model_field, field, ComparisonOperator.LOWER_THAN
+        )
+
+
+class HasNotValueLowerThanFilterType(NotViewFilterTypeMixin, HasValueLowerThanFilter):
+    type = "has_not_value_lower"
+
+
+class HasValueLowerOrEqualThanFilter(ComparisonHasValueFilter):
+    type = "has_value_lower_or_equal"
+    compatible_field_types = [
+        FormulaFieldType.compatible_with_formula_types(
+            FormulaFieldType.array_of(BaserowFormulaNumberType.type)
+        ),
+    ]
+
+    def get_filter_expression(self, field_name, value, model_field, field):
+        field_type: HasNumericValueComparableToFilterSupport = (
+            field_type_registry.get_by_model(field)
+        )
+        return field_type.get_has_numeric_value_comparable_to_filter_query(
+            field_name,
+            value,
+            model_field,
+            field,
+            ComparisonOperator.LOWER_THAN_OR_EQUAL,
+        )
+
+
+class HasNotValueLowerOrEqualTHanFilterType(
+    NotViewFilterTypeMixin, HasValueLowerOrEqualThanFilter
+):
+    type = "has_not_value_lower_or_equal"
diff --git a/backend/src/baserow/contrib/database/views/view_filters.py b/backend/src/baserow/contrib/database/views/view_filters.py
index bb4058dbb..711dc31b6 100644
--- a/backend/src/baserow/contrib/database/views/view_filters.py
+++ b/backend/src/baserow/contrib/database/views/view_filters.py
@@ -2,10 +2,8 @@ import datetime as datetime_module
 import zoneinfo
 from collections import defaultdict
 from datetime import date, datetime, timedelta
-from decimal import Decimal
 from enum import Enum
 from functools import reduce
-from math import ceil, floor
 from types import MappingProxyType
 from typing import Any, Dict, NamedTuple, Optional, Tuple, Union
 
@@ -124,8 +122,9 @@ class EqualViewFilterType(ViewFilterType):
             return Q()
 
         # Check if the model_field accepts the value.
+        field_type = field_type_registry.get_by_model(field)
         try:
-            value = model_field.get_prep_value(value)
+            value = field_type.prepare_filter_value(field, model_field, value)
             return Q(**{field_name: value})
         except Exception:
             return self.default_filter_on_exception()
@@ -379,9 +378,6 @@ class NumericComparisonViewFilterType(ViewFilterType):
         ),
     ]
 
-    def should_round_value_to_compare(self, value, model_field):
-        return isinstance(model_field, IntegerField) and value.find(".") != -1
-
     def get_filter(self, field_name, value, model_field, field):
         value = value.strip()
 
@@ -389,17 +385,14 @@ class NumericComparisonViewFilterType(ViewFilterType):
         if value == "":
             return Q()
 
-        if self.should_round_value_to_compare(value, model_field):
-            decimal_value = Decimal(value)
-            value = self.rounding_func(decimal_value)
-
-        # Check if the model_field accepts the value.
+        field_type = field_type_registry.get_by_model(field)
         try:
-            value = model_field.get_prep_value(value)
-            return Q(**{f"{field_name}__{self.operator}": value})
-        except Exception:
+            filter_value = field_type.prepare_filter_value(field, model_field, value)
+        except ValueError:
             return self.default_filter_on_exception()
 
+        return Q(**{f"{field_name}__{self.operator}": filter_value})
+
 
 class LowerThanViewFilterType(NumericComparisonViewFilterType):
     """
@@ -409,7 +402,6 @@ class LowerThanViewFilterType(NumericComparisonViewFilterType):
 
     type = "lower_than"
     operator = "lt"
-    rounding_func = floor
 
 
 class LowerThanOrEqualViewFilterType(NumericComparisonViewFilterType):
@@ -421,7 +413,6 @@ class LowerThanOrEqualViewFilterType(NumericComparisonViewFilterType):
 
     type = "lower_than_or_equal"
     operator = "lte"
-    rounding_func = floor
 
 
 class HigherThanViewFilterType(NumericComparisonViewFilterType):
@@ -432,7 +423,6 @@ class HigherThanViewFilterType(NumericComparisonViewFilterType):
 
     type = "higher_than"
     operator = "gt"
-    rounding_func = ceil
 
 
 class HigherThanOrEqualViewFilterType(NumericComparisonViewFilterType):
@@ -444,7 +434,6 @@ class HigherThanOrEqualViewFilterType(NumericComparisonViewFilterType):
 
     type = "higher_than_or_equal"
     operator = "gte"
-    rounding_func = ceil
 
 
 class TimezoneAwareDateViewFilterType(ViewFilterType):
@@ -1217,24 +1206,15 @@ class BooleanViewFilterType(ViewFilterType):
     ]
 
     def get_filter(self, field_name, value, model_field, field):
-        value = value.strip().lower()
-        value = value in [
-            "y",
-            "t",
-            "o",
-            "yes",
-            "true",
-            "on",
-            "1",
-        ]
+        if value == "":  # consider emtpy value as False
+            value = "false"
 
-        # Check if the model_field accepts the value.
-        # noinspection PyBroadException
+        field_type = field_type_registry.get_by_model(field)
         try:
-            value = model_field.get_prep_value(value)
+            value = field_type.prepare_filter_value(field, model_field, value)
             return Q(**{field_name: value})
-        except Exception:
-            return Q()
+        except ValueError:
+            return self.default_filter_on_exception()
 
 
 class ManyToManyHasBaseViewFilter(ViewFilterType):
diff --git a/backend/tests/baserow/contrib/database/field/test_number_lookup_field_filters.py b/backend/tests/baserow/contrib/database/field/test_number_lookup_field_filters.py
new file mode 100644
index 000000000..6ecacffd0
--- /dev/null
+++ b/backend/tests/baserow/contrib/database/field/test_number_lookup_field_filters.py
@@ -0,0 +1,561 @@
+import typing
+from functools import partial
+
+import pytest
+
+from tests.baserow.contrib.database.utils import (
+    number_field_factory,
+    setup_linked_table_and_lookup,
+    text_field_factory,
+)
+
+if typing.TYPE_CHECKING:
+    from baserow.test_utils.fixtures import Fixtures
+
+
+def number_lookup_filter_proc(
+    data_fixture: "Fixtures",
+    filter_type_name: str,
+    test_value: str,
+    expected_rows: set[str],
+    number_decimal_places: int = 5,
+):
+    """
+    Common numeric lookup field test procedure. Each test operates on a fixed set of
+    data, where each table row contains a lookup field with a predefined set of linked
+    rows.
+    """
+
+    t = setup_linked_table_and_lookup(
+        data_fixture,
+        target_field_factory=partial(
+            number_field_factory,
+            number_decimal_places=number_decimal_places,
+            number_negative=True,
+        ),
+        helper_fields_table=[partial(text_field_factory, name="row name")],
+    )
+
+    row_name_field = t.model.get_field_object_by_user_field_name("row name")["field"]
+    link_row_name = t.link_row_field.db_column
+    target_name = t.target_field.db_column
+    lookup_name = t.lookup_field.db_column
+
+    row_name = row_name_field.db_column
+
+    row_values = [
+        "1000.0001",  # 0
+        "1000",
+        "999.999",
+        "123.45",
+        "100",
+        "50",  # 5
+        "1.000001",  # will be rounded to 1.00000
+        "1",
+        "0.00007",
+        "0.00004",
+        "0.00001",  # 10
+        "0.0000",
+        "-0.1",  # mind that it will be rounded to 0.10000
+        "-2",
+        None,  # 14
+        "999.99999999",
+    ]
+    dict_rows = [{target_name: rval} for rval in row_values]
+
+    linked_rows = t.row_handler.create_rows(
+        user=t.user, table=t.other_table, rows_values=dict_rows
+    )
+
+    # helper to get linked rows by indexes
+    def get_linked_rows(*indexes) -> list[int]:
+        return [linked_rows[idx].id for idx in indexes]
+
+    rows = [
+        {row_name: "above 100", link_row_name: get_linked_rows(0, 1, 2, 3)},
+        {row_name: "exact 100", link_row_name: get_linked_rows(4)},
+        {row_name: "between 100 and 10", link_row_name: get_linked_rows(4, 5)},
+        {
+            row_name: "between 10 and 0",
+            link_row_name: get_linked_rows(6, 7, 8, 9, 10, 11),
+        },
+        {row_name: "zero", link_row_name: get_linked_rows(11)},
+        {row_name: "below zero", link_row_name: get_linked_rows(12, 13)},
+        {row_name: "nineninenine", link_row_name: get_linked_rows(2)},
+        {row_name: "onetwothree", link_row_name: get_linked_rows(3)},
+        # no refs is a tricky one - we don't have is_empty for numeric field
+        {row_name: "no refs", link_row_name: []},
+        {row_name: "refs with empty", link_row_name: get_linked_rows(14)},
+        {row_name: "100 with empty", link_row_name: get_linked_rows(4, 14)},
+        {row_name: "ninieninenine rounded", link_row_name: get_linked_rows(15)},
+    ]
+
+    t.row_handler.create_rows(user=t.user, table=t.table, rows_values=rows)
+
+    clean_query = t.view_handler.get_queryset(t.grid_view)
+
+    t.view_handler.create_filter(
+        t.user,
+        t.grid_view,
+        field=t.lookup_field,
+        type_name=filter_type_name,
+        value=test_value,
+    )
+
+    q = t.view_handler.get_queryset(t.grid_view)
+    print(f"filter {filter_type_name} with value: {(test_value,)}")
+    print(f"expected: {expected_rows}")
+    print(f"filtered: {[getattr(item, row_name) for item in q]}")
+    for item in q:
+        print(f" {item.id} -> {getattr(item, row_name)}: {getattr(item, lookup_name)}")
+    print()
+    for item in clean_query:
+        print(f" {item.id} -> {getattr(item, row_name)}: {getattr(item, lookup_name)}")
+    assert len(q) == len(expected_rows)
+    assert set([getattr(r, row_name) for r in q]) == set(expected_rows)
+
+
+ALL_ROW_NAMES = [
+    "above 100",
+    "exact 100",
+    "between 100 and 10",
+    "between 10 and 0",
+    "zero",
+    "below zero",
+    "nineninenine",
+    "onetwothree",
+    "no refs",
+    "refs with empty",
+    "100 with empty",
+    "ninieninenine rounded",
+]
+
+
+@pytest.mark.parametrize(
+    "filter_type_name,expected_rows",
+    [
+        ("has_empty_value", ["refs with empty", "100 with empty"]),
+        (
+            "has_not_empty_value",
+            [
+                "above 100",
+                "exact 100",
+                "between 100 and 10",
+                "between 10 and 0",
+                "zero",
+                "below zero",
+                "nineninenine",
+                "ninieninenine rounded",
+                "onetwothree",
+                "no refs",  # this is due to inversion of has_empty_value
+                # "refs with empty", "100 with empty"
+            ],
+        ),
+    ],
+)
+@pytest.mark.django_db
+def test_number_lookup_field_has_empty_value_filter(
+    data_fixture, filter_type_name, expected_rows
+):
+    return number_lookup_filter_proc(data_fixture, filter_type_name, "", expected_rows)
+
+
+@pytest.mark.parametrize(
+    "filter_type_name,test_value,expected_rows",
+    [
+        ("has_value_equal", "", ALL_ROW_NAMES),
+        ("has_value_equal", "invalid", []),
+        # too large, the same as invalid
+        (
+            "has_value_equal",
+            "1" + ("0" * 40),
+            [],
+        ),
+        (
+            "has_value_equal",
+            "100",
+            ["exact 100", "between 100 and 10", "100 with empty"],
+        ),
+        # no rounding to 0.00001
+        ("has_value_equal", "100.00000000001", []),
+        ("has_not_value_equal", "", ALL_ROW_NAMES),
+        ("has_not_value_equal", "invalid", ALL_ROW_NAMES),
+        (
+            "has_not_value_equal",
+            "1" + ("0" * 40),
+            ALL_ROW_NAMES,
+        ),
+        ("has_not_value_equal", "999", ALL_ROW_NAMES),
+        ("has_not_value_equal", "1.00000001", ALL_ROW_NAMES),
+        (
+            "has_not_value_equal",
+            "0.0",
+            [o for o in ALL_ROW_NAMES if o not in {"zero", "between 10 and 0"}],
+        ),
+    ],
+)
+@pytest.mark.django_db
+def test_number_lookup_field_has_value_equal_filter(
+    data_fixture, filter_type_name, test_value, expected_rows
+):
+    return number_lookup_filter_proc(
+        data_fixture, filter_type_name, test_value, expected_rows
+    )
+
+
+@pytest.mark.parametrize(
+    "filter_type_name,test_value,expected_rows",
+    [
+        ("has_value_contains", "", ALL_ROW_NAMES),
+        ("has_value_contains", "invalid", []),
+        (
+            "has_value_contains",
+            "1" + ("0" * 40),
+            [],
+        ),
+        (
+            "has_value_contains",
+            "100",
+            # includes '0.10000'
+            [
+                "above 100",
+                "exact 100",
+                "between 100 and 10",
+                "below zero",
+                "100 with empty",
+                "ninieninenine rounded",
+            ],
+        ),
+        ("has_value_contains", "999", ["nineninenine", "above 100"]),
+        ("has_value_contains", "99.999", ["nineninenine", "above 100"]),
+        ("has_not_value_contains", "", ALL_ROW_NAMES),
+        ("has_not_value_contains", "invalid", ALL_ROW_NAMES),
+        (
+            "has_not_value_contains",
+            "1" + ("0" * 40),
+            ALL_ROW_NAMES,
+        ),
+        (
+            "has_not_value_contains",
+            "999",
+            [o for o in ALL_ROW_NAMES if o not in {"nineninenine", "above 100"}],
+        ),
+        ("has_not_value_contains", "1.00000001", ALL_ROW_NAMES),
+        (
+            "has_not_value_contains",
+            "001",
+            # between 10 and 0 contains 1.00001
+            # above 100 contains 1000.0001
+            [o for o in ALL_ROW_NAMES if o not in {"between 10 and 0", "above 100"}],
+        ),
+    ],
+)
+@pytest.mark.django_db
+def test_number_lookup_field_has_value_contains_filter(
+    data_fixture, filter_type_name, test_value, expected_rows
+):
+    return number_lookup_filter_proc(
+        data_fixture, filter_type_name, test_value, expected_rows
+    )
+
+
+@pytest.mark.parametrize(
+    "filter_type_name,test_value,expected_rows",
+    [
+        ("has_value_higher", "", ALL_ROW_NAMES),
+        ("has_value_higher", "invalid", []),
+        (
+            "has_value_higher",
+            "1" + ("0" * 40),
+            [],
+        ),
+        (
+            "has_value_higher",
+            "-1" + ("0" * 40),
+            [o for o in ALL_ROW_NAMES if o not in {"no refs", "refs with empty"}],
+        ),
+        (
+            "has_value_higher",
+            "-0.0000001",
+            [
+                "above 100",
+                "exact 100",
+                "between 100 and 10",
+                "between 10 and 0",
+                "zero",
+                "nineninenine",
+                "onetwothree",
+                "100 with empty",
+                "ninieninenine rounded",
+            ],
+        ),
+        (
+            "has_value_higher",
+            "0.00000001",
+            [
+                "above 100",
+                "exact 100",
+                "between 100 and 10",
+                "between 10 and 0",
+                "nineninenine",
+                "onetwothree",
+                "100 with empty",
+                "ninieninenine rounded",
+            ],
+        ),
+        (
+            "has_value_higher",
+            "999.998999",  # not rounded
+            ["above 100", "nineninenine", "ninieninenine rounded"],
+        ),
+        ("has_value_higher", "999.999", ["above 100", "ninieninenine rounded"]),
+    ],
+)
+@pytest.mark.django_db
+def test_number_lookup_field_has_value_higher_than_filter(
+    data_fixture, filter_type_name, test_value, expected_rows
+):
+    return number_lookup_filter_proc(
+        data_fixture, filter_type_name, test_value, expected_rows
+    )
+
+
+@pytest.mark.parametrize(
+    "filter_type_name,test_value,expected_rows",
+    [
+        ("has_value_higher_or_equal", "", ALL_ROW_NAMES),
+        ("has_value_higher_or_equal", "invalid", []),
+        (
+            "has_value_higher_or_equal",
+            "1" + ("0" * 40),
+            [],
+        ),
+        (
+            "has_value_higher_or_equal",
+            "-1" + ("0" * 40),
+            [o for o in ALL_ROW_NAMES if o not in {"no refs", "refs with empty"}],
+        ),
+        (
+            "has_value_higher_or_equal",
+            "-0.0",
+            [
+                "above 100",
+                "exact 100",
+                "between 100 and 10",
+                "between 10 and 0",
+                "zero",
+                "nineninenine",
+                "onetwothree",
+                "100 with empty",
+                "ninieninenine rounded",
+            ],
+        ),
+        (
+            "has_value_higher_or_equal",
+            "999.999",
+            ["above 100", "nineninenine", "ninieninenine rounded"],
+        ),
+        ("has_not_value_higher_or_equal", "", ALL_ROW_NAMES),
+        (
+            "has_not_value_higher_or_equal",
+            "invalid",
+            ALL_ROW_NAMES,
+        ),  # reversed has_value_higher_or_equal
+        (
+            "has_not_value_higher_or_equal",
+            "1" + ("0" * 40),
+            ALL_ROW_NAMES,  # reversed has_value_higher_or_equal
+        ),
+        (
+            "has_not_value_higher_or_equal",
+            "-1" + ("0" * 40),
+            [o for o in ALL_ROW_NAMES if o in {"no refs", "refs with empty"}],
+        ),
+        (
+            "has_not_value_higher_or_equal",
+            "-0.0",
+            ["below zero", "no refs", "refs with empty"],
+        ),
+        (
+            "has_not_value_higher_or_equal",
+            "999.999",
+            [
+                o
+                for o in ALL_ROW_NAMES
+                if o not in {"above 100", "nineninenine", "ninieninenine rounded"}
+            ],
+        ),
+    ],
+)
+@pytest.mark.django_db
+def test_number_lookup_field_has_value_higher_equal_than_filter(
+    data_fixture, filter_type_name, test_value, expected_rows
+):
+    return number_lookup_filter_proc(
+        data_fixture, filter_type_name, test_value, expected_rows
+    )
+
+
+@pytest.mark.parametrize(
+    "filter_type_name,test_value,expected_rows",
+    [
+        ("has_value_lower_or_equal", "", ALL_ROW_NAMES),
+        ("has_value_lower_or_equal", "invalid", []),
+        (
+            "has_value_lower_or_equal",
+            "1" + ("0" * 40),
+            [o for o in ALL_ROW_NAMES if o not in {"no refs", "refs with empty"}],
+        ),
+        (
+            "has_value_lower_or_equal",
+            "-1" + ("0" * 40),
+            [],
+        ),
+        (
+            "has_value_lower_or_equal",
+            "-0.0",
+            ["between 10 and 0", "zero", "below zero"],
+        ),
+        (
+            "has_value_lower_or_equal",
+            "999.999",
+            [
+                "above 100",
+                "exact 100",
+                "between 100 and 10",
+                "between 10 and 0",
+                "zero",
+                "below zero",
+                "nineninenine",
+                "onetwothree",
+                "100 with empty",
+            ],
+        ),
+        ("has_not_value_lower_or_equal", "", ALL_ROW_NAMES),
+        (
+            "has_not_value_lower_or_equal",
+            "invalid",
+            ALL_ROW_NAMES,
+        ),  # reversed has_value_lower_or_equal
+        (
+            "has_not_value_lower_or_equal",
+            "1" + ("0" * 40),
+            ["no refs", "refs with empty"],  # reversed has_value_lower_or_equal
+        ),
+        (
+            "has_not_value_lower_or_equal",
+            "-1" + ("0" * 40),
+            ALL_ROW_NAMES,
+        ),
+        (
+            "has_not_value_lower_or_equal",
+            "-0.0",
+            [
+                "above 100",
+                "exact 100",
+                "between 100 and 10",
+                "nineninenine",
+                "onetwothree",
+                "no refs",
+                "refs with empty",
+                "100 with empty",
+                "ninieninenine rounded",
+            ],
+        ),
+        (
+            "has_not_value_lower_or_equal",
+            "999.999",
+            ["no refs", "refs with empty", "ninieninenine rounded"],
+        ),
+    ],
+)
+@pytest.mark.django_db
+def test_number_lookup_field_has_value_lower_equal_than_filter(
+    data_fixture, filter_type_name, test_value, expected_rows
+):
+    return number_lookup_filter_proc(
+        data_fixture, filter_type_name, test_value, expected_rows
+    )
+
+
+@pytest.mark.parametrize(
+    "filter_type_name,test_value,expected_rows",
+    [
+        ("has_value_lower", "", ALL_ROW_NAMES),
+        ("has_value_lower", "invalid", []),
+        (
+            "has_value_lower",
+            "1" + ("0" * 40),
+            [o for o in ALL_ROW_NAMES if o not in {"no refs", "refs with empty"}],
+        ),
+        (
+            "has_value_lower",
+            "-1" + ("0" * 40),
+            [],
+        ),
+        (
+            "has_value_lower",
+            "-0.0",
+            ["below zero"],
+        ),
+        (
+            "has_value_lower",
+            "999.999",
+            [
+                "above 100",
+                "exact 100",
+                "between 100 and 10",
+                "between 10 and 0",
+                "zero",
+                "below zero",
+                "onetwothree",
+                "100 with empty",
+            ],
+        ),
+        ("has_not_value_lower", "", ALL_ROW_NAMES),
+        (
+            "has_not_value_lower",
+            "invalid",
+            ALL_ROW_NAMES,
+        ),  # reversed has_value_lower
+        (
+            "has_not_value_lower",
+            "1" + ("0" * 40),
+            ["no refs", "refs with empty"],  # reversed has_value_lower
+        ),
+        (
+            "has_not_value_lower",
+            "-1" + ("0" * 40),
+            ALL_ROW_NAMES,
+        ),
+        (
+            "has_not_value_lower",
+            "-0.0",
+            [
+                "above 100",
+                "exact 100",
+                "between 100 and 10",
+                "between 10 and 0",
+                "zero",
+                "nineninenine",
+                "onetwothree",
+                "no refs",
+                "refs with empty",
+                "100 with empty",
+                "ninieninenine rounded",
+            ],
+        ),
+        (
+            "has_not_value_lower",
+            "999.999",
+            ["nineninenine", "no refs", "refs with empty", "ninieninenine rounded"],
+        ),
+    ],
+)
+@pytest.mark.django_db
+def test_number_lookup_field_has_value_lower_than_filter(
+    data_fixture, filter_type_name, test_value, expected_rows
+):
+    return number_lookup_filter_proc(
+        data_fixture, filter_type_name, test_value, expected_rows
+    )
diff --git a/backend/tests/baserow/contrib/database/utils.py b/backend/tests/baserow/contrib/database/utils.py
index 9c7155bc1..94cd60626 100644
--- a/backend/tests/baserow/contrib/database/utils.py
+++ b/backend/tests/baserow/contrib/database/utils.py
@@ -142,13 +142,18 @@ def text_field_value_factory(data_fixture, target_field, value=None):
 
 
 def setup_linked_table_and_lookup(
-    data_fixture, target_field_factory
+    data_fixture,
+    target_field_factory,
+    helper_fields_other_table: Iterable[Callable] = frozenset(),
+    helper_fields_table: Iterable[Callable] = frozenset(),
 ) -> LookupFieldSetup:
     user = data_fixture.create_user()
     database = data_fixture.create_database_application(user=user)
     table = data_fixture.create_database_table(user=user, database=database)
     other_table = data_fixture.create_database_table(user=user, database=database)
     target_field = target_field_factory(data_fixture, other_table, user)
+    for helper_field_factory in helper_fields_other_table:
+        helper_field_factory(data_fixture, table=other_table, user=user)
     link_row_field = data_fixture.create_link_row_field(
         name="link", table=table, link_row_table=other_table
     )
@@ -160,6 +165,8 @@ def setup_linked_table_and_lookup(
         target_field_name=target_field.name,
         setup_dependencies=False,
     )
+    for helper_field_factory in helper_fields_table:
+        helper_field_factory(data_fixture, table=table, user=user)
     grid_view = data_fixture.create_grid_view(table=table)
     view_handler = ViewHandler()
     row_handler = RowHandler()
diff --git a/backend/tests/baserow/contrib/database/view/test_view_array_filters.py b/backend/tests/baserow/contrib/database/view/test_view_array_filters.py
index 958b993bb..e89fd0a73 100644
--- a/backend/tests/baserow/contrib/database/view/test_view_array_filters.py
+++ b/backend/tests/baserow/contrib/database/view/test_view_array_filters.py
@@ -1629,12 +1629,17 @@ def test_has_value_length_is_lower_than_uuid_field_types(data_fixture):
         (
             "has_all_values_equal",
             "",
-            [BooleanLookupRow.ALL_FALSE],
+            [
+                BooleanLookupRow.ALL_TRUE,
+                BooleanLookupRow.ALL_FALSE,
+                BooleanLookupRow.NO_VALUES,
+                BooleanLookupRow.MIXED,
+            ],
         ),
         (
             "has_all_values_equal",
             "invalid",
-            [BooleanLookupRow.ALL_FALSE],
+            [],
         ),
     ],
 )
@@ -1683,12 +1688,17 @@ def test_has_all_values_equal_filter_boolean_lookup_field_type(
         (
             "has_value_equal",
             "",
-            [BooleanLookupRow.MIXED, BooleanLookupRow.ALL_FALSE],
+            [
+                BooleanLookupRow.MIXED,
+                BooleanLookupRow.ALL_FALSE,
+                BooleanLookupRow.NO_VALUES,
+                BooleanLookupRow.ALL_TRUE,
+            ],
         ),
         (
             "has_value_equal",
             "invalid",
-            [BooleanLookupRow.MIXED, BooleanLookupRow.ALL_FALSE],
+            [],
         ),
     ],
 )
@@ -1737,12 +1747,23 @@ def test_has_value_equal_filter_boolean_lookup_field_type(
         (
             "has_not_value_equal",
             "",
-            [BooleanLookupRow.ALL_TRUE, BooleanLookupRow.NO_VALUES],
+            [
+                BooleanLookupRow.MIXED,
+                BooleanLookupRow.ALL_FALSE,
+                BooleanLookupRow.NO_VALUES,
+                BooleanLookupRow.ALL_TRUE,
+            ],
         ),
         (
             "has_not_value_equal",
             "invalid",
-            [BooleanLookupRow.ALL_TRUE, BooleanLookupRow.NO_VALUES],
+            # inverse of has_value_equal with `invalid` value
+            [
+                BooleanLookupRow.ALL_TRUE,
+                BooleanLookupRow.ALL_FALSE,
+                BooleanLookupRow.NO_VALUES,
+                BooleanLookupRow.MIXED,
+            ],
         ),
     ],
 )
diff --git a/changelog/entries/unreleased/feature/3112_number_lookup_field_filters.json b/changelog/entries/unreleased/feature/3112_number_lookup_field_filters.json
new file mode 100644
index 000000000..53aa6058e
--- /dev/null
+++ b/changelog/entries/unreleased/feature/3112_number_lookup_field_filters.json
@@ -0,0 +1,7 @@
+{
+    "type": "feature",
+    "message": "Number lookup field filters",
+    "issue_number": 3112,
+    "bullet_points": [],
+    "created_at": "2024-12-13"
+}
\ No newline at end of file
diff --git a/web-frontend/modules/database/arrayFilterMixins.js b/web-frontend/modules/database/arrayFilterMixins.js
index 2f331db40..b83be66f4 100644
--- a/web-frontend/modules/database/arrayFilterMixins.js
+++ b/web-frontend/modules/database/arrayFilterMixins.js
@@ -5,6 +5,8 @@ import {
   genericHasEmptyValueFilter,
   genericHasValueLengthLowerThanFilter,
   genericHasAllValuesEqualFilter,
+  numericHasValueComparableToFilterFunction,
+  ComparisonOperator,
 } from '@baserow/modules/database/utils/fieldFilters'
 
 export const hasEmptyValueFilterMixin = {
@@ -24,6 +26,7 @@ export const hasAllValuesEqualFilterMixin = {
       this.getHasAllValuesEqualFilterFunction(field)(cellValue, filterValue)
     )
   },
+
   hasNotAllValuesEqualFilter(cellValue, filterValue, field) {
     return (
       filterValue === '' ||
@@ -91,11 +94,107 @@ export const hasValueLengthIsLowerThanFilterMixin = {
   },
 }
 
-export const formulaArrayFilterMixin = {
-  getSubType(field) {
-    return this.app.$registry.get('formula_type', field.array_formula_type)
+export const hasNumericValueComparableToFilterMixin = {
+  // equal to
+  getHasValueEqualFilterFunction(field) {
+    return numericHasValueComparableToFilterFunction(ComparisonOperator.EQUAL)
   },
 
+  hasValueEqualFilter(cellValue, filterValue, field) {
+    return (
+      filterValue === '' ||
+      this.getHasValueEqualFilterFunction(field)(cellValue, filterValue)
+    )
+  },
+
+  hasNotValueEqualFilter(cellValue, filterValue, field) {
+    return (
+      filterValue === '' ||
+      !this.getHasValueEqualFilterFunction(field)(cellValue, filterValue)
+    )
+  },
+
+  /**
+   * All other comparison operators: higher_than, lower_than, etc.
+   */
+  hasValueComparableToFilter(
+    cellValue,
+    filterValue,
+    field,
+    comparisonOperator
+  ) {
+    return numericHasValueComparableToFilterFunction(comparisonOperator)(
+      cellValue,
+      filterValue
+    )
+  },
+}
+
+/*
+ * Mixin for the FormulaField to handle the array formula filters for number fields.
+ */
+export const formulaFieldArrayFilterMixin = Object.assign(
+  {},
+  hasAllValuesEqualFilterMixin,
+  hasEmptyValueFilterMixin,
+  hasValueEqualFilterMixin,
+  hasValueContainsFilterMixin,
+  hasValueContainsWordFilterMixin,
+  hasValueLengthIsLowerThanFilterMixin,
+  {
+    getHasAllValuesEqualFilterFunction(field) {
+      return this.getFormulaType(field)?.getHasAllValuesEqualFilterFunction(
+        field
+      )
+    },
+
+    getHasEmptyValueFilterFunction(field) {
+      return this.getFormulaType(field)?.getHasEmptyValueFilterFunction(field)
+    },
+
+    getHasValueEqualFilterFunction(field) {
+      return this.getFormulaType(field)?.getHasValueEqualFilterFunction(field)
+    },
+
+    getHasValueContainsFilterFunction(field) {
+      return this.getFormulaType(field)?.getHasValueContainsFilterFunction(
+        field
+      )
+    },
+
+    getHasValueContainsWordFilterFunction(field) {
+      return this.getFormulaType(field)?.getHasValueContainsWordFilterFunction(
+        field
+      )
+    },
+
+    getHasValueLengthIsLowerThanFilterFunction(field) {
+      return this.getFormulaType(
+        field
+      )?.getHasValueLengthIsLowerThanFilterFunction(field)
+    },
+
+    hasValueComparableToFilter(
+      cellValue,
+      filterValue,
+      field,
+      comparisonOperator
+    ) {
+      return this.getFormulaType(field)?.hasValueComparableToFilter(
+        cellValue,
+        filterValue,
+        field,
+        comparisonOperator
+      )
+    },
+  }
+)
+
+/*
+ * Mixin for the BaserowFormulaArrayType to proxy all the array filters to the
+ * correct sub type.
+ */
+export const baserowFormulaArrayTypeFilterMixin = {
   getHasEmptyValueFilterFunction(field) {
     const subType = this.getSubType(field)
     return subType.getHasEmptyValueFilterFunction(field)
@@ -144,6 +243,20 @@ export const formulaArrayFilterMixin = {
   getHasAllValuesEqualFilterFunction(field) {
     return this.getSubType(field)?.getHasAllValuesEqualFilterFunction(field)
   },
+
+  hasValueComparableToFilter(
+    cellValue,
+    filterValue,
+    field,
+    comparisonOperator
+  ) {
+    return this.getSubType(field)?.hasValueComparableToFilter(
+      cellValue,
+      filterValue,
+      field,
+      comparisonOperator
+    )
+  },
 }
 
 export const hasSelectOptionIdEqualMixin = Object.assign(
diff --git a/web-frontend/modules/database/arrayViewFilters.js b/web-frontend/modules/database/arrayViewFilters.js
index 444df7214..a0c34f213 100644
--- a/web-frontend/modules/database/arrayViewFilters.js
+++ b/web-frontend/modules/database/arrayViewFilters.js
@@ -4,20 +4,8 @@ import { FormulaFieldType } from '@baserow/modules/database/fieldTypes'
 import { ViewFilterType } from '@baserow/modules/database/viewFilters'
 import viewFilterTypeText from '@baserow/modules/database/components/view/ViewFilterTypeText.vue'
 import ViewFilterTypeMultipleSelectOptions from '@baserow/modules/database/components/view/ViewFilterTypeMultipleSelectOptions'
-import _ from 'lodash'
-
-/**
- * This function normalizes boolean values that may be used internally in the filter
- * to values that can be transferred to the backend.
- * @param value
- * @returns {string|*}
- */
-const normalizeBooleanForFilters = (value) => {
-  if (!_.isBoolean(value)) {
-    return value
-  }
-  return value ? '1' : '0'
-}
+import { BaserowFormulaNumberType } from '@baserow/modules/database/formula/formulaTypes'
+import { ComparisonOperator } from '@baserow/modules/database//utils/fieldFilters'
 
 export class HasEmptyValueViewFilterType extends ViewFilterType {
   static getType() {
@@ -35,6 +23,7 @@ export class HasEmptyValueViewFilterType extends ViewFilterType {
       FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
       FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
       FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
+      FormulaFieldType.compatibleWithFormulaTypes('array(number)'),
     ]
   }
 
@@ -59,6 +48,7 @@ export class HasNotEmptyValueViewFilterType extends ViewFilterType {
       FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
       FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
       FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
+      FormulaFieldType.compatibleWithFormulaTypes('array(number)'),
     ]
   }
 
@@ -77,22 +67,11 @@ export class HasValueEqualViewFilterType extends ViewFilterType {
     return i18n.t('viewFilter.hasValueEqual')
   }
 
-  getDefaultValue(field) {
-    // has_value_equal filter by default sends an empty string. For consistency
-    // a default value should be in pair with a default value from the input component.
-    return this.prepareValue('', field)
-  }
-
   matches(cellValue, filterValue, field, fieldType) {
-    filterValue = fieldType.parseInputValue(field, filterValue)
+    filterValue = fieldType.prepareFilterValue(field, filterValue)
     return fieldType.hasValueEqualFilter(cellValue, filterValue, field)
   }
 
-  prepareValue(value, field) {
-    const fieldType = this.app.$registry.get('field', field.type)
-    return normalizeBooleanForFilters(fieldType.parseInputValue(field, value))
-  }
-
   getInputComponent(field) {
     const fieldType = this.app.$registry.get('field', field.type)
     return fieldType.getFilterInputComponent(field, this) || viewFilterTypeText
@@ -105,7 +84,8 @@ export class HasValueEqualViewFilterType extends ViewFilterType {
         FormulaFieldType.arrayOf('char'),
         FormulaFieldType.arrayOf('url'),
         FormulaFieldType.arrayOf('boolean'),
-        FormulaFieldType.arrayOf('single_select')
+        FormulaFieldType.arrayOf('single_select'),
+        FormulaFieldType.arrayOf('number')
       ),
     ]
   }
@@ -122,21 +102,10 @@ export class HasNotValueEqualViewFilterType extends ViewFilterType {
   }
 
   matches(cellValue, filterValue, field, fieldType) {
-    filterValue = fieldType.parseInputValue(field, filterValue)
+    filterValue = fieldType.prepareFilterValue(field, filterValue)
     return fieldType.hasNotValueEqualFilter(cellValue, filterValue, field)
   }
 
-  getDefaultValue(field) {
-    // has_not_value_equal filter by default sends an empty string. For consistency
-    // a default value should be in pair with a default value from the input component.
-    return this.prepareValue('', field)
-  }
-
-  prepareValue(value, field) {
-    const fieldType = this.app.$registry.get('field', field.type)
-    return normalizeBooleanForFilters(fieldType.parseInputValue(field, value))
-  }
-
   getInputComponent(field) {
     const fieldType = this.app.$registry.get('field', field.type)
     return fieldType.getFilterInputComponent(field, this) || viewFilterTypeText
@@ -149,7 +118,8 @@ export class HasNotValueEqualViewFilterType extends ViewFilterType {
         FormulaFieldType.arrayOf('char'),
         FormulaFieldType.arrayOf('url'),
         FormulaFieldType.arrayOf('boolean'),
-        FormulaFieldType.arrayOf('single_select')
+        FormulaFieldType.arrayOf('single_select'),
+        FormulaFieldType.arrayOf('number')
       ),
     ]
   }
@@ -171,10 +141,13 @@ export class HasValueContainsViewFilterType extends ViewFilterType {
 
   getCompatibleFieldTypes() {
     return [
-      FormulaFieldType.compatibleWithFormulaTypes('array(text)'),
-      FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
-      FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
-      FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
+      FormulaFieldType.compatibleWithFormulaTypes(
+        FormulaFieldType.arrayOf('char'),
+        FormulaFieldType.arrayOf('text'),
+        FormulaFieldType.arrayOf('url'),
+        FormulaFieldType.arrayOf('single_select'),
+        FormulaFieldType.arrayOf('number')
+      ),
     ]
   }
 
@@ -199,10 +172,13 @@ export class HasNotValueContainsViewFilterType extends ViewFilterType {
 
   getCompatibleFieldTypes() {
     return [
-      FormulaFieldType.compatibleWithFormulaTypes('array(text)'),
-      FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
-      FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
-      FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
+      FormulaFieldType.compatibleWithFormulaTypes(
+        FormulaFieldType.arrayOf('char'),
+        FormulaFieldType.arrayOf('text'),
+        FormulaFieldType.arrayOf('url'),
+        FormulaFieldType.arrayOf('single_select'),
+        FormulaFieldType.arrayOf('number')
+      ),
     ]
   }
 
@@ -321,7 +297,7 @@ export class HasAllValuesEqualViewFilterType extends ViewFilterType {
   }
 
   matches(cellValue, filterValue, field, fieldType) {
-    filterValue = fieldType.parseInputValue(field, filterValue)
+    filterValue = fieldType.prepareFilterValue(field, filterValue)
     return fieldType.hasAllValuesEqualFilter(cellValue, filterValue, field)
   }
 }
@@ -371,3 +347,243 @@ export class HasNoneSelectOptionEqualViewFilterType extends ViewFilterType {
     return fieldType.hasNotValueEqualFilter(cellValue, filterValue, field)
   }
 }
+
+export class HasValueHigherThanViewFilterType extends ViewFilterType {
+  static getType() {
+    return 'has_value_higher'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasValueHigherThan')
+  }
+
+  getInputComponent(field) {
+    return ViewFilterTypeNumber
+  }
+
+  getCompatibleFieldTypes() {
+    return [
+      FormulaFieldType.compatibleWithFormulaTypes(
+        FormulaFieldType.arrayOf(BaserowFormulaNumberType.getType())
+      ),
+    ]
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    filterValue = fieldType.prepareFilterValue(field, filterValue)
+    return (
+      filterValue === '' ||
+      fieldType.hasValueComparableToFilter(
+        cellValue,
+        filterValue,
+        field,
+        ComparisonOperator.HIGHER_THAN
+      )
+    )
+  }
+}
+
+export class HasNotValueHigherThanViewFilterType extends HasValueHigherThanViewFilterType {
+  static getType() {
+    return 'has_not_value_higher'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasNotValueHigherThan')
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    filterValue = fieldType.prepareFilterValue(field, filterValue)
+    return (
+      filterValue === '' ||
+      !fieldType.hasValueComparableToFilter(
+        cellValue,
+        filterValue,
+        field,
+        ComparisonOperator.HIGHER_THAN
+      )
+    )
+  }
+}
+
+export class HasValueHigherThanOrEqualViewFilterType extends ViewFilterType {
+  static getType() {
+    return 'has_value_higher_or_equal'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasValueHigherThanOrEqual')
+  }
+
+  getInputComponent(field) {
+    return ViewFilterTypeNumber
+  }
+
+  getCompatibleFieldTypes() {
+    return [
+      FormulaFieldType.compatibleWithFormulaTypes(
+        FormulaFieldType.arrayOf(BaserowFormulaNumberType.getType())
+      ),
+    ]
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    filterValue = fieldType.prepareFilterValue(field, filterValue)
+    return (
+      filterValue === '' ||
+      fieldType.hasValueComparableToFilter(
+        cellValue,
+        filterValue,
+        field,
+        ComparisonOperator.HIGHER_THAN_OR_EQUAL
+      )
+    )
+  }
+}
+
+export class HasNotValueHigherThanOrEqualViewFilterType extends HasValueHigherThanOrEqualViewFilterType {
+  static getType() {
+    return 'has_not_value_higher_or_equal'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasNotValueHigherThanOrEqual')
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    filterValue = fieldType.prepareFilterValue(field, filterValue)
+    return (
+      filterValue === '' ||
+      !fieldType.hasValueComparableToFilter(
+        cellValue,
+        filterValue,
+        field,
+        ComparisonOperator.HIGHER_THAN_OR_EQUAL
+      )
+    )
+  }
+}
+
+export class HasValueLowerThanViewFilterType extends ViewFilterType {
+  static getType() {
+    return 'has_value_lower'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasValueLowerThan')
+  }
+
+  getInputComponent(field) {
+    return ViewFilterTypeNumber
+  }
+
+  getCompatibleFieldTypes() {
+    return [
+      FormulaFieldType.compatibleWithFormulaTypes(
+        FormulaFieldType.arrayOf(BaserowFormulaNumberType.getType())
+      ),
+    ]
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    filterValue = fieldType.prepareFilterValue(field, filterValue)
+    return (
+      filterValue === '' ||
+      fieldType.hasValueComparableToFilter(
+        cellValue,
+        filterValue,
+        field,
+        ComparisonOperator.LOWER_THAN
+      )
+    )
+  }
+}
+
+export class HasNotValueLowerThanViewFilterType extends HasValueLowerThanViewFilterType {
+  static getType() {
+    return 'has_not_value_lower'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasNotValueLowerThan')
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    filterValue = fieldType.prepareFilterValue(field, filterValue)
+    return (
+      filterValue === '' ||
+      !fieldType.hasValueComparableToFilter(
+        cellValue,
+        filterValue,
+        field,
+        ComparisonOperator.LOWER_THAN
+      )
+    )
+  }
+}
+
+export class HasValueLowerThanOrEqualViewFilterType extends ViewFilterType {
+  static getType() {
+    return 'has_value_lower_or_equal'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasValueLowerThanOrEqual')
+  }
+
+  getInputComponent(field) {
+    return ViewFilterTypeNumber
+  }
+
+  getCompatibleFieldTypes() {
+    return [
+      FormulaFieldType.compatibleWithFormulaTypes(
+        FormulaFieldType.arrayOf(BaserowFormulaNumberType.getType())
+      ),
+    ]
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    filterValue = fieldType.prepareFilterValue(field, filterValue)
+    return (
+      filterValue === '' ||
+      fieldType.hasValueComparableToFilter(
+        cellValue,
+        filterValue,
+        field,
+        ComparisonOperator.LOWER_THAN_OR_EQUAL
+      )
+    )
+  }
+}
+
+export class HasNotValueLowerThanOrEqualViewFilterType extends HasValueLowerThanOrEqualViewFilterType {
+  static getType() {
+    return 'has_not_value_lower_or_equal'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasNotValueLowerThanOrEqual')
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    filterValue = fieldType.prepareFilterValue(field, filterValue)
+    return (
+      filterValue === '' ||
+      !fieldType.hasValueComparableToFilter(
+        cellValue,
+        filterValue,
+        field,
+        ComparisonOperator.LOWER_THAN_OR_EQUAL
+      )
+    )
+  }
+}
diff --git a/web-frontend/modules/database/components/view/ViewFilterTypeNumber.vue b/web-frontend/modules/database/components/view/ViewFilterTypeNumber.vue
index 7a5a57421..25985940c 100644
--- a/web-frontend/modules/database/components/view/ViewFilterTypeNumber.vue
+++ b/web-frontend/modules/database/components/view/ViewFilterTypeNumber.vue
@@ -20,6 +20,12 @@ import numberField from '@baserow/modules/database/mixins/numberField'
 export default {
   name: 'ViewFilterTypeNumber',
   mixins: [filterTypeInput, numberField],
+  data() {
+    return {
+      // Avoid rounding decimals to ensure filter values match backend behavior.
+      roundDecimals: false,
+    }
+  },
   watch: {
     field: {
       handler() {
diff --git a/web-frontend/modules/database/fieldTypes.js b/web-frontend/modules/database/fieldTypes.js
index 983e26ddf..4ee50bdcf 100644
--- a/web-frontend/modules/database/fieldTypes.js
+++ b/web-frontend/modules/database/fieldTypes.js
@@ -15,14 +15,7 @@ import {
   isValidEmail,
   isValidURL,
 } from '@baserow/modules/core/utils/string'
-import {
-  hasValueContainsFilterMixin,
-  hasValueEqualFilterMixin,
-  hasValueContainsWordFilterMixin,
-  hasValueLengthIsLowerThanFilterMixin,
-  hasEmptyValueFilterMixin,
-  hasAllValuesEqualFilterMixin,
-} from '@baserow/modules/database/arrayFilterMixins'
+import { formulaFieldArrayFilterMixin } from '@baserow/modules/database/arrayFilterMixins'
 import {
   parseNumberValue,
   formatNumberValue,
@@ -709,10 +702,25 @@ export class FieldType extends Registerable {
     )
   }
 
+  /**
+   * Returns optionally input component for a field / filter type combination.
+   * This is called by FilterType to get the component. FilterType should provide
+   * a default if FieldType returns null.
+   *
+   * @returns {null}
+   */
   getFilterInputComponent(field, filterType) {
     return null
   }
 
+  /**
+   * Return a valid filter value for the field type. This is used to parse the
+   * filter value from the frontend to the backend.
+   */
+  prepareFilterValue(field, filterValue) {
+    return filterValue
+  }
+
   /**
    * Is called for each field in the row when another field value in the row has
    * changed. Optionally, a different value can be returned here for that field. This
@@ -1643,6 +1651,11 @@ export class NumberFieldType extends FieldType {
     }
     return new BigNumber(value)
   }
+
+  prepareFilterValue(field, value) {
+    const res = parseNumberValue(field, String(value ?? ''), false)
+    return res === null || res.isNaN() ? '' : res.toString()
+  }
 }
 
 BigNumber.config({ EXPONENTIAL_AT: NumberFieldType.getMaxNumberLength() })
@@ -1914,6 +1927,10 @@ export class BooleanFieldType extends FieldType {
   getHasNotValueEqualFilterFunction(field) {
     return this.getHasValueEqualFilterFunction(field, true)
   }
+
+  prepareFilterValue(field, value) {
+    return this.parseInputValue(field, String(value ?? ''))
+  }
 }
 
 class BaseDateFieldType extends FieldType {
@@ -3745,12 +3762,7 @@ export class PhoneNumberFieldType extends FieldType {
 }
 
 export class FormulaFieldType extends mix(
-  hasAllValuesEqualFilterMixin,
-  hasEmptyValueFilterMixin,
-  hasValueEqualFilterMixin,
-  hasValueContainsFilterMixin,
-  hasValueContainsWordFilterMixin,
-  hasValueLengthIsLowerThanFilterMixin,
+  formulaFieldArrayFilterMixin,
   FieldType
 ) {
   static getType() {
@@ -3792,7 +3804,11 @@ export class FormulaFieldType extends mix(
     return i18n.t('fieldType.formula')
   }
 
-  getFormulaSubtype(field) {
+  prepareFilterValue(field, value) {
+    return this.getFormulaType(field)?.prepareFilterValue(field, value)
+  }
+
+  getFormulaType(field) {
     return this.app.$registry.get('formula_type', field.formula_type)
   }
 
@@ -3813,7 +3829,7 @@ export class FormulaFieldType extends mix(
   }
 
   getFilterInputComponent(field, filterType) {
-    return this.getFormulaSubtype(field)?.getFilterInputComponent(
+    return this.getFormulaType(field)?.getFilterInputComponent(
       field,
       filterType
     )
@@ -3824,15 +3840,15 @@ export class FormulaFieldType extends mix(
   }
 
   getCardValueHeight(field) {
-    return this.getFormulaSubtype(field)?.getCardComponent().height || 0
+    return this.getFormulaType(field)?.getCardComponent().height || 0
   }
 
   getCanSortInView(field) {
-    return this.getFormulaSubtype(field)?.getCanSortInView(field)
+    return this.getFormulaType(field)?.getCanSortInView(field)
   }
 
   getSort(name, order, field) {
-    return this.getFormulaSubtype(field)?.getSort(name, order, field)
+    return this.getFormulaType(field)?.getSort(name, order, field)
   }
 
   getEmptyValue(field) {
@@ -3840,7 +3856,7 @@ export class FormulaFieldType extends mix(
   }
 
   getDocsDataType(field) {
-    return this.getFormulaSubtype(field)?.getDocsDataType(field)
+    return this.getFormulaType(field)?.getDocsDataType(field)
   }
 
   getDocsDescription(field) {
@@ -3852,11 +3868,11 @@ export class FormulaFieldType extends mix(
   }
 
   getDocsResponseExample(field) {
-    return this.getFormulaSubtype(field)?.getDocsResponseExample(field)
+    return this.getFormulaType(field)?.getDocsResponseExample(field)
   }
 
   prepareValueForCopy(field, value) {
-    return this.getFormulaSubtype(field)?.prepareValueForCopy(field, value)
+    return this.getFormulaType(field)?.prepareValueForCopy(field, value)
   }
 
   getContainsFilterFunction(field) {
@@ -3876,11 +3892,11 @@ export class FormulaFieldType extends mix(
   }
 
   toHumanReadableString(field, value) {
-    return this.getFormulaSubtype(field)?.toHumanReadableString(field, value)
+    return this.getFormulaType(field)?.toHumanReadableString(field, value)
   }
 
   getSortIndicator(field) {
-    return this.getFormulaSubtype(field)?.getSortIndicator(field)
+    return this.getFormulaType(field)?.getSortIndicator(field)
   }
 
   getFormComponent() {
@@ -3912,57 +3928,25 @@ export class FormulaFieldType extends mix(
   }
 
   canRepresentDate(field) {
-    return this.getFormulaSubtype(field)?.canRepresentDate(field)
+    return this.getFormulaType(field)?.canRepresentDate(field)
   }
 
   getCanGroupByInView(field) {
-    return this.getFormulaSubtype(field)?.canGroupByInView(field)
+    return this.getFormulaType(field)?.canGroupByInView(field)
   }
 
   parseInputValue(field, value) {
-    const underlyingFieldType = this.getFormulaSubtype(field)
+    const underlyingFieldType = this.getFormulaType(field)
     return underlyingFieldType.parseInputValue(field, value)
   }
 
   parseFromLinkedRowItemValue(field, value) {
-    const underlyingFieldType = this.getFormulaSubtype(field)
+    const underlyingFieldType = this.getFormulaType(field)
     return underlyingFieldType.parseFromLinkedRowItemValue(field, value)
   }
 
   canRepresentFiles(field) {
-    return this.getFormulaSubtype(field)?.canRepresentFiles(field)
-  }
-
-  getHasAllValuesEqualFilterFunction(field) {
-    return this.getFormulaSubtype(field)?.getHasAllValuesEqualFilterFunction(
-      field
-    )
-  }
-
-  getHasEmptyValueFilterFunction(field) {
-    return this.getFormulaSubtype(field)?.getHasEmptyValueFilterFunction(field)
-  }
-
-  getHasValueEqualFilterFunction(field) {
-    return this.getFormulaSubtype(field)?.getHasValueEqualFilterFunction(field)
-  }
-
-  getHasValueContainsFilterFunction(field) {
-    return this.getFormulaSubtype(field)?.getHasValueContainsFilterFunction(
-      field
-    )
-  }
-
-  getHasValueContainsWordFilterFunction(field) {
-    return this.getFormulaSubtype(field)?.getHasValueContainsWordFilterFunction(
-      field
-    )
-  }
-
-  getHasValueLengthIsLowerThanFilterFunction(field) {
-    return this.getFormulaSubtype(
-      field
-    )?.getHasValueLengthIsLowerThanFilterFunction(field)
+    return this.getFormulaType(field)?.canRepresentFiles(field)
   }
 }
 
diff --git a/web-frontend/modules/database/formula/formulaTypes.js b/web-frontend/modules/database/formula/formulaTypes.js
index 4f4f6338d..853db626b 100644
--- a/web-frontend/modules/database/formula/formulaTypes.js
+++ b/web-frontend/modules/database/formula/formulaTypes.js
@@ -57,7 +57,8 @@ import {
   hasSelectOptionIdEqualMixin,
   hasSelectOptionValueContainsFilterMixin,
   hasSelectOptionValueContainsWordFilterMixin,
-  formulaArrayFilterMixin,
+  baserowFormulaArrayTypeFilterMixin,
+  hasNumericValueComparableToFilterMixin,
 } from '@baserow/modules/database/arrayFilterMixins'
 import _ from 'lodash'
 import ViewFilterTypeBoolean from '@baserow/modules/database/components/view/ViewFilterTypeBoolean.vue'
@@ -81,18 +82,22 @@ export class BaserowFormulaTypeDefinition extends Registerable {
     )
   }
 
-  /**
-   * Returns optionally input component for a field / filter type combination
-   * @returns {null}
-   */
   getFilterInputComponent(field, filterType) {
-    return null
+    return this.app.$registry
+      .get('field', this.getFieldType())
+      .getFilterInputComponent(field, filterType)
   }
 
   getRowEditArrayFieldComponent() {
     return null
   }
 
+  prepareFilterValue(field, value) {
+    return this.app.$registry
+      .get('field', this.getFieldType())
+      .prepareFilterValue(field, value)
+  }
+
   getFunctionalGridViewFieldComponent() {
     return this.app.$registry
       .get('field', this.getFieldType())
@@ -305,7 +310,13 @@ export class BaserowFormulaCharType extends mix(
   }
 }
 
-export class BaserowFormulaNumberType extends BaserowFormulaTypeDefinition {
+export class BaserowFormulaNumberType extends mix(
+  hasEmptyValueFilterMixin,
+  hasValueContainsFilterMixin,
+  hasNumericValueComparableToFilterMixin,
+
+  BaserowFormulaTypeDefinition
+) {
   static getType() {
     return 'number'
   }
@@ -582,7 +593,7 @@ export class BaserowFormulaInvalidType extends BaserowFormulaTypeDefinition {
 }
 
 export class BaserowFormulaArrayType extends mix(
-  formulaArrayFilterMixin,
+  baserowFormulaArrayTypeFilterMixin,
   BaserowFormulaTypeDefinition
 ) {
   static getType() {
@@ -605,6 +616,14 @@ export class BaserowFormulaArrayType extends mix(
     return RowCardFieldArray
   }
 
+  prepareFilterValue(field, value) {
+    return this.getSubType(field)?.prepareFilterValue(field, value)
+  }
+
+  getSubType(field) {
+    return this.app.$registry.get('formula_type', field.array_formula_type)
+  }
+
   getRowEditFieldComponent(field) {
     const arrayOverride =
       this.getSubType(field)?.getRowEditArrayFieldComponent()
diff --git a/web-frontend/modules/database/locales/en.json b/web-frontend/modules/database/locales/en.json
index 39fa46773..7f1186bcb 100644
--- a/web-frontend/modules/database/locales/en.json
+++ b/web-frontend/modules/database/locales/en.json
@@ -580,7 +580,15 @@
   },
   "viewFilter": {
     "filter": "Filter | 1 Filter | {count} Filters",
-    "hasAllValuesEqual": "has all values equal"
+    "hasAllValuesEqual": "has all values equal",
+    "hasValueHigherThan": "has value higher than",
+    "hasValueHigherThanOrEqual": "has value higher than or equal",
+    "hasValueLowerThan": "has value lower than",
+    "hasValueLowerThanOrEqual": "has value lower than or equal",
+    "hasNotValueHigherThan": "doesn't have value higher than",
+    "hasNotValueHigherThanOrEqual": "doesn't have value higher than or equal",
+    "hasNotValueLowerThan": "doesn't have value lower than",
+    "hasNotValueLowerThanOrEqual": "doesn't have value lower than or equal"
   },
   "viewContext": {
     "exportView": "Export view",
diff --git a/web-frontend/modules/database/mixins/numberField.js b/web-frontend/modules/database/mixins/numberField.js
index 6186b25b5..c2d0b8cf4 100644
--- a/web-frontend/modules/database/mixins/numberField.js
+++ b/web-frontend/modules/database/mixins/numberField.js
@@ -16,6 +16,7 @@ export default {
     return {
       formattedValue: '',
       focused: false,
+      roundDecimals: true,
     }
   },
   methods: {
@@ -23,7 +24,7 @@ export default {
       this.formattedValue = this.formatNumberValue(field, value)
     },
     formatNumberValue(field, value) {
-      return formatNumberValue(field, value)
+      return formatNumberValue(field, value, true, this.roundDecimals)
     },
     /*
      * This method is similar to formatNumberValue, but it returns the value as a
@@ -33,10 +34,15 @@ export default {
      */
     formatNumberValueForEdit(field, value) {
       const withThousandSeparator = false
-      return formatNumberValue(field, value, withThousandSeparator)
+      return formatNumberValue(
+        field,
+        value,
+        withThousandSeparator,
+        this.roundDecimals
+      )
     },
     parseNumberValue(field, value) {
-      return parseNumberValue(field, value)
+      return parseNumberValue(field, value, this.roundDecimals)
     },
     getNumberFormatOptions(field) {
       return getNumberFormatOptions(field)
diff --git a/web-frontend/modules/database/plugin.js b/web-frontend/modules/database/plugin.js
index e3a164ccc..9ab044ab7 100644
--- a/web-frontend/modules/database/plugin.js
+++ b/web-frontend/modules/database/plugin.js
@@ -108,6 +108,14 @@ import {
   HasAllValuesEqualViewFilterType,
   HasAnySelectOptionEqualViewFilterType,
   HasNoneSelectOptionEqualViewFilterType,
+  HasValueLowerThanViewFilterType,
+  HasValueLowerThanOrEqualViewFilterType,
+  HasValueHigherThanViewFilterType,
+  HasValueHigherThanOrEqualViewFilterType,
+  HasNotValueLowerThanOrEqualViewFilterType,
+  HasNotValueLowerThanViewFilterType,
+  HasNotValueHigherThanOrEqualViewFilterType,
+  HasNotValueHigherThanViewFilterType,
 } from '@baserow/modules/database/arrayViewFilters'
 import {
   CSVImporterType,
@@ -581,6 +589,40 @@ export default (context) => {
   app.$registry.register('viewFilter', new NotEmptyViewFilterType(context))
   app.$registry.register('viewFilter', new UserIsFilterType(context))
   app.$registry.register('viewFilter', new UserIsNotFilterType(context))
+  app.$registry.register(
+    'viewFilter',
+    new HasValueHigherThanViewFilterType(context)
+  )
+  app.$registry.register(
+    'viewFilter',
+    new HasNotValueHigherThanViewFilterType(context)
+  )
+  app.$registry.register(
+    'viewFilter',
+    new HasValueHigherThanOrEqualViewFilterType(context)
+  )
+  app.$registry.register(
+    'viewFilter',
+    new HasNotValueHigherThanOrEqualViewFilterType(context)
+  )
+
+  app.$registry.register(
+    'viewFilter',
+    new HasValueLowerThanViewFilterType(context)
+  )
+
+  app.$registry.register(
+    'viewFilter',
+    new HasNotValueLowerThanViewFilterType(context)
+  )
+  app.$registry.register(
+    'viewFilter',
+    new HasValueLowerThanOrEqualViewFilterType(context)
+  )
+  app.$registry.register(
+    'viewFilter',
+    new HasNotValueLowerThanOrEqualViewFilterType(context)
+  )
 
   app.$registry.register(
     'viewOwnershipType',
diff --git a/web-frontend/modules/database/utils/fieldFilters.js b/web-frontend/modules/database/utils/fieldFilters.js
index 7ddb24eb1..81256b280 100644
--- a/web-frontend/modules/database/utils/fieldFilters.js
+++ b/web-frontend/modules/database/utils/fieldFilters.js
@@ -4,6 +4,7 @@
 //    list of file names, we don't want the filterValue to accidentally match the end
 //    of one filename and the start of another.
 import _ from 'lodash'
+import BigNumber from 'bignumber.js'
 
 export function filenameContainsFilter(
   rowValue,
@@ -31,8 +32,8 @@ export function genericContainsFilter(
   if (humanReadableRowValue == null) {
     return false
   }
-  humanReadableRowValue = humanReadableRowValue.toString().toLowerCase().trim()
-  filterValue = filterValue.toString().toLowerCase().trim()
+  humanReadableRowValue = String(humanReadableRowValue).toLowerCase().trim()
+  filterValue = String(filterValue).toLowerCase().trim()
 
   return humanReadableRowValue.includes(filterValue)
 }
@@ -45,8 +46,8 @@ export function genericContainsWordFilter(
   if (humanReadableRowValue == null) {
     return false
   }
-  humanReadableRowValue = humanReadableRowValue.toString().toLowerCase().trim()
-  filterValue = filterValue.toString().toLowerCase().trim()
+  humanReadableRowValue = String(humanReadableRowValue).toLowerCase().trim()
+  filterValue = String(filterValue).toLowerCase().trim()
   // check using regex to match whole words
   // make sure to escape the filterValue as it may contain regex special characters
   filterValue = filterValue.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&')
@@ -100,10 +101,10 @@ export function genericHasValueContainsFilter(cellValue, filterValue) {
     return false
   }
 
-  filterValue = filterValue.toString().toLowerCase().trim()
+  filterValue = String(filterValue).toLowerCase().trim()
 
   for (let i = 0; i < cellValue.length; i++) {
-    const value = cellValue[i].value.toString().toLowerCase().trim()
+    const value = String(cellValue[i].value).toLowerCase().trim()
 
     if (value.includes(filterValue)) {
       return true
@@ -118,14 +119,14 @@ export function genericHasValueContainsWordFilter(cellValue, filterValue) {
     return false
   }
 
-  filterValue = filterValue.toString().toLowerCase().trim()
+  filterValue = String(filterValue).toLowerCase().trim()
   filterValue = filterValue.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&')
 
   for (let i = 0; i < cellValue.length; i++) {
     if (cellValue[i].value == null) {
       continue
     }
-    const value = cellValue[i].value.toString().toLowerCase().trim()
+    const value = String(cellValue[i].value).toLowerCase().trim()
     if (value.match(new RegExp(`\\b${filterValue}\\b`))) {
       return true
     }
@@ -143,7 +144,7 @@ export function genericHasValueLengthLowerThanFilter(cellValue, filterValue) {
     if (cellValue[i].value == null) {
       continue
     }
-    const valueLength = cellValue[i].value.toString().length
+    const valueLength = String(cellValue[i].value).length
     if (valueLength < filterValue) {
       return true
     }
@@ -151,3 +152,53 @@ export function genericHasValueLengthLowerThanFilter(cellValue, filterValue) {
 
   return false
 }
+
+export const ComparisonOperator = {
+  EQUAL: '=',
+  HIGHER_THAN: '>',
+  HIGHER_THAN_OR_EQUAL: '>=',
+  LOWER_THAN: '<',
+  LOWER_THAN_OR_EQUAL: '<=',
+}
+
+function doNumericArrayComparison(cellValue, filterValue, compareFunc) {
+  const filterNr = new BigNumber(filterValue)
+  if (!Array.isArray(cellValue) || filterNr.isNaN()) {
+    return false
+  }
+  return _.some(_.map(cellValue, 'value'), (item) =>
+    compareFunc(new BigNumber(item), filterNr)
+  )
+}
+
+export function numericHasValueComparableToFilterFunction(comparisonOp) {
+  return (cellValue, filterValue) => {
+    let compareFunc
+    switch (comparisonOp) {
+      case ComparisonOperator.EQUAL:
+        compareFunc = (a, b) => a.isEqualTo(b)
+        break
+      case ComparisonOperator.HIGHER_THAN:
+        compareFunc = (a, b) => a.isGreaterThan(b)
+        break
+      case ComparisonOperator.HIGHER_THAN_OR_EQUAL:
+        compareFunc = (a, b) => a.isGreaterThanOrEqualTo(b)
+        break
+      case ComparisonOperator.LOWER_THAN:
+        compareFunc = (a, b) => a.isLessThan(b)
+        break
+      case ComparisonOperator.LOWER_THAN_OR_EQUAL:
+        compareFunc = (a, b) => a.isLessThanOrEqualTo(b)
+        break
+    }
+    if (compareFunc === undefined) {
+      throw new Error('Invalid comparison operator')
+    }
+
+    return doNumericArrayComparison(
+      cellValue,
+      filterValue,
+      (arrayItemNr, filterNr) => compareFunc(arrayItemNr, filterNr)
+    )
+  }
+}
diff --git a/web-frontend/modules/database/utils/number.js b/web-frontend/modules/database/utils/number.js
index cf8be9e6e..30990218b 100644
--- a/web-frontend/modules/database/utils/number.js
+++ b/web-frontend/modules/database/utils/number.js
@@ -12,6 +12,8 @@ const DECIMAL_SEPARATORS = {
   PERIOD: '.',
 }
 
+const NUMBER_MAX_DECIMAL_PLACES = 10
+
 const DEFAULT_THOUSAND_SEPARATOR = THOUSAND_SEPARATORS.NONE
 const DEFAULT_DECIMAL_SEPARATOR = DECIMAL_SEPARATORS.PERIOD
 
@@ -57,7 +59,7 @@ export const getNumberFormatOptions = (field) => {
 
   const numberPrefix = field.number_prefix ?? ''
   const numberSuffix = field.number_suffix ?? ''
-  const decimalPlaces = field.number_decimal_places ?? 0
+  const decimalPlaces = field.number_decimal_places ?? undefined
   const allowNegative = field.number_negative ?? false
 
   return {
@@ -79,7 +81,8 @@ export const getNumberFormatOptions = (field) => {
 export const formatNumberValue = (
   field,
   value,
-  withThousandSeparator = true
+  withThousandSeparator = true,
+  roundDecimals = true
 ) => {
   if (value === null || value === undefined || value === '') {
     return ''
@@ -95,7 +98,9 @@ export const formatNumberValue = (
 
   // Parse the input value if it's a string
   let numericValue =
-    typeof value === 'string' ? parseNumberValue(field, value) : value
+    typeof value === 'string'
+      ? parseNumberValue(field, value, roundDecimals)
+      : value
 
   if (numericValue === null) {
     return null
@@ -116,9 +121,13 @@ export const formatNumberValue = (
     locale = 'en-US'
     localeThousandsSeparator = DECIMAL_SEPARATORS.COMMA
   }
+  // Format the number, but keep all decimal places if roundDecimals is false.
+  // For example, filter values are not rounded since the backend doesn't round them.
   const formatter = new Intl.NumberFormat(locale, {
     minimumFractionDigits: decimalPlaces,
-    maximumFractionDigits: decimalPlaces,
+    maximumFractionDigits: roundDecimals
+      ? decimalPlaces
+      : NUMBER_MAX_DECIMAL_PLACES,
     useGrouping: true,
   })
   let formatted = formatter.format(numericValue)
@@ -139,18 +148,22 @@ export const formatNumberValue = (
   return `${sign}${numberPrefix}${formatted}${numberSuffix}`.trim()
 }
 
-export const parseNumberValue = (field, value) => {
+export const parseNumberValue = (field, value, roundDecimals = true) => {
   const { numberPrefix, numberSuffix, decimalSeparator } =
     getNumberFormatOptions(field)
 
-  if (value === null || value === undefined || value === '') {
+  if (value == null || value === '') {
     return null
   }
 
   const toBigNumber = (val) => {
-    return new BigNumber(
-      new BigNumber(val).toFixed(field.number_decimal_places)
-    )
+    let rounded = val
+    if (roundDecimals) {
+      rounded = new BigNumber(val).decimalPlaces(
+        field.number_decimal_places ?? 0
+      )
+    }
+    return new BigNumber(rounded)
   }
 
   if (typeof value === 'number' || BigNumber.isBigNumber(value)) {
@@ -192,5 +205,5 @@ export const parseNumberValue = (field, value) => {
   }
 
   const parsedNumber = toBigNumber(result)
-  return isNaN(parsedNumber) ? null : isNegative ? -parsedNumber : parsedNumber
+  return parsedNumber.isNaN() ? null : isNegative ? -parsedNumber : parsedNumber
 }
diff --git a/web-frontend/test/unit/database/arrayViewFiltersMatch.spec.js b/web-frontend/test/unit/database/arrayViewFiltersMatch.spec.js
index 9f1aa61cf..c0d5cc14a 100644
--- a/web-frontend/test/unit/database/arrayViewFiltersMatch.spec.js
+++ b/web-frontend/test/unit/database/arrayViewFiltersMatch.spec.js
@@ -10,8 +10,15 @@ import {
   HasNotEmptyValueViewFilterType,
   HasValueLengthIsLowerThanViewFilterType,
   HasAllValuesEqualViewFilterType,
+  HasValueHigherThanViewFilterType,
+  HasValueHigherThanOrEqualViewFilterType,
+  HasNotValueHigherThanViewFilterType,
+  HasNotValueHigherThanOrEqualViewFilterType,
 } from '@baserow/modules/database/arrayViewFilters'
-import { FormulaFieldType } from '@baserow/modules/database/fieldTypes'
+import {
+  FormulaFieldType,
+  LookupFieldType,
+} from '@baserow/modules/database/fieldTypes'
 import {
   EmptyViewFilterType,
   NotEmptyViewFilterType,
@@ -998,3 +1005,344 @@ describe('Boolean-based array view filters', () => {
     }
   )
 })
+
+describe('Number-based array view filters', () => {
+  let testApp = null
+  let fieldType = null
+
+  const fieldDefinition = {
+    type: 'lookup',
+    formula_type: 'array',
+    array_formula_type: 'number',
+  }
+
+  beforeAll(() => {
+    testApp = new TestApp()
+    fieldType = new LookupFieldType({
+      app: testApp._app,
+    })
+  })
+
+  afterEach(() => {
+    testApp.afterEach()
+  })
+
+  const hasEmptyValueTestCases = [
+    {
+      cellValue: [],
+      filterValue: '',
+      expected: false,
+    },
+    {
+      cellValue: [{ value: null }],
+      filterValue: '',
+      expected: true,
+    },
+    {
+      cellValue: [{ value: '123.0' }, { value: null }],
+      filterValue: '',
+      expected: true,
+    },
+    {
+      cellValue: [{ value: '123.0' }, { value: '' }],
+      filterValue: '',
+      expected: true,
+    },
+  ]
+
+  const hasValueContainsTestCases = [
+    {
+      cellValue: [],
+      filterValue: '',
+      expected: { has: true, hasNot: true },
+    },
+    {
+      cellValue: [],
+      filterValue: null,
+      expected: { has: false, hasNot: true },
+    },
+    {
+      cellValue: [{ value: null }],
+      filterValue: '',
+      expected: { has: true, hasNot: true },
+    },
+    {
+      cellValue: [{ value: '' }],
+      filterValue: '',
+      expected: { has: true, hasNot: true },
+    },
+    {
+      cellValue: [{ value: null }],
+      filterValue: null,
+      expected: { has: true, hasNot: false },
+    },
+
+    {
+      cellValue: [{ value: 123.0 }, { value: null }],
+      filterValue: '123',
+      expected: { has: true, hasNot: false },
+    },
+    {
+      cellValue: [{ value: '123.0' }, { value: null }],
+      filterValue: '123',
+      expected: { has: true, hasNot: false },
+    },
+    {
+      cellValue: [{ value: 123.0 }, { value: '' }],
+      filterValue: '100',
+      expected: { has: false, hasNot: true },
+    },
+    {
+      cellValue: [{ value: 1.11101 }],
+      filterValue: '10',
+      expected: { has: true, hasNot: false },
+    },
+    {
+      cellValue: [{ value: '1.11101' }],
+      filterValue: '10',
+      expected: { has: true, hasNot: false },
+    },
+    {
+      cellValue: [{ value: 10100.001 }],
+      filterValue: '20',
+      expected: { has: false, hasNot: true },
+    },
+  ]
+
+  // has value higher/lower are implemented with a code path that will return
+  // true for empty filterValues. This requires per filter type result check
+  const hasValueHigherThanOrEqualTestCases = [
+    {
+      cellValue: [],
+      filterValue: '',
+      expected: {
+        higher: true,
+        higherEqual: true,
+        notHigher: true,
+        notHigherEqual: true,
+      },
+    },
+    {
+      cellValue: [],
+      filterValue: null,
+      expected: {
+        higher: true,
+        higherEqual: true,
+        notHigher: true,
+        notHigherEqual: true,
+      },
+    },
+    {
+      cellValue: [{ value: null }],
+      filterValue: '',
+      expected: {
+        higher: true,
+        higherEqual: true,
+        notHigher: true,
+        notHigherEqual: true,
+      },
+    },
+    {
+      cellValue: [{ value: '' }],
+      filterValue: '',
+      expected: {
+        higher: true,
+        higherEqual: true,
+        notHigher: true,
+        notHigherEqual: true,
+      },
+    },
+    {
+      cellValue: [{ value: null }],
+      filterValue: 10,
+      expected: {
+        higher: false,
+        higherEqual: false,
+        notHigher: true,
+        notHigherEqual: true,
+      },
+    },
+    {
+      cellValue: [{ value: null }],
+      filterValue: 10,
+      expected: {
+        higher: false,
+        higherEqual: false,
+        notHigher: true,
+        notHigherEqual: true,
+      },
+    },
+    {
+      cellValue: [{ value: 123.0 }, { value: null }],
+      filterValue: '123',
+      expected: {
+        higher: false,
+        higherEqual: true,
+        notHigher: true,
+        notHigherEqual: false,
+      },
+    },
+
+    {
+      cellValue: [{ value: '123.0' }, { value: null }],
+      filterValue: '123',
+      expected: {
+        higher: false,
+        higherEqual: true,
+        notHigher: true,
+        notHigherEqual: false,
+      },
+    },
+
+    {
+      cellValue: [{ value: 123.0 }],
+      filterValue: '123.0001',
+      expected: {
+        higher: false,
+        higherEqual: false,
+        notHigher: true,
+        notHigherEqual: true,
+      },
+    },
+    {
+      cellValue: [{ value: '123.0' }],
+      filterValue: '123.0001',
+      expected: {
+        higher: false,
+        higherEqual: false,
+        notHigher: true,
+        notHigherEqual: true,
+      },
+    },
+    {
+      cellValue: [{ value: 123.0 }, { value: 500 }],
+      filterValue: '123.0001',
+      expected: {
+        higher: true,
+        higherEqual: true,
+        notHigher: false,
+        notHigherEqual: false,
+      },
+    },
+  ]
+
+  test.each(hasEmptyValueTestCases)(
+    'hasEmptyValueTestCases %j',
+    (testValues) => {
+      const result = new HasEmptyValueViewFilterType({
+        app: testApp._app,
+      }).matches(
+        testValues.cellValue,
+        testValues.filterValue,
+        fieldDefinition,
+        fieldType
+      )
+      expect(result).toBe(testValues.expected)
+    }
+  )
+
+  test.each(hasEmptyValueTestCases)(
+    'hasNotEmptyValueTestCases %j',
+    (testValues) => {
+      const result = new HasNotEmptyValueViewFilterType({
+        app: testApp._app,
+      }).matches(
+        testValues.cellValue,
+        testValues.filterValue,
+        fieldDefinition,
+        fieldType
+      )
+      expect(result).toBe(!testValues.expected)
+    }
+  )
+
+  test.each(hasValueContainsTestCases)(
+    'hasValueContainsTestCases %j',
+    (testValues) => {
+      const result = new HasValueContainsViewFilterType({
+        app: testApp._app,
+      }).matches(
+        testValues.cellValue,
+        testValues.filterValue,
+        fieldDefinition,
+        fieldType
+      )
+      expect(result).toBe(testValues.expected.has)
+    }
+  )
+
+  test.each(hasValueContainsTestCases)(
+    'hasNotValueContainsTestCases %j',
+    (testValues) => {
+      const result = new HasNotValueContainsViewFilterType({
+        app: testApp._app,
+      }).matches(
+        testValues.cellValue,
+        testValues.filterValue,
+        fieldDefinition,
+        fieldType
+      )
+      expect(result).toBe(testValues.expected.hasNot)
+    }
+  )
+
+  test.each(hasValueHigherThanOrEqualTestCases)(
+    'hasValueHigherTestCases %j',
+    (testValues) => {
+      const result = new HasValueHigherThanViewFilterType({
+        app: testApp._app,
+      }).matches(
+        testValues.cellValue,
+        testValues.filterValue,
+        fieldDefinition,
+        fieldType
+      )
+      expect(result).toBe(testValues.expected.higher)
+    }
+  )
+
+  test.each(hasValueHigherThanOrEqualTestCases)(
+    'hasValueHigherOrEqualTestCases %j',
+    (testValues) => {
+      const result = new HasValueHigherThanOrEqualViewFilterType({
+        app: testApp._app,
+      }).matches(
+        testValues.cellValue,
+        testValues.filterValue,
+        fieldDefinition,
+        fieldType
+      )
+      expect(result).toBe(testValues.expected.higherEqual)
+    }
+  )
+
+  test.each(hasValueHigherThanOrEqualTestCases)(
+    'hasNotValueHigherTestCases %j',
+    (testValues) => {
+      const result = new HasNotValueHigherThanViewFilterType({
+        app: testApp._app,
+      }).matches(
+        testValues.cellValue,
+        testValues.filterValue,
+        fieldDefinition,
+        fieldType
+      )
+      expect(result).toBe(testValues.expected.notHigher)
+    }
+  )
+
+  test.each(hasValueHigherThanOrEqualTestCases)(
+    'hasNotValueHigherOrEqualTestCases %j',
+    (testValues) => {
+      const result = new HasNotValueHigherThanOrEqualViewFilterType({
+        app: testApp._app,
+      }).matches(
+        testValues.cellValue,
+        testValues.filterValue,
+        fieldDefinition,
+        fieldType
+      )
+      expect(result).toBe(testValues.expected.notHigherEqual)
+    }
+  )
+})