diff --git a/Cargo.lock b/Cargo.lock
index ec15debf6a9ea8892583c4123421766dfe93c063..462a6b9e0f4ae0c80a47cd9c5aa0e7d261449d71 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -856,9 +856,21 @@ dependencies = [
  "num_cpus",
  "pin-project-lite",
  "socket2 0.5.5",
+ "tokio-macros",
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "tokio-macros"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "tokio-native-tls"
 version = "0.3.1"
diff --git a/Cargo.toml b/Cargo.toml
index 43c82de473964f74db82b807dff2f191652d4d17..f57ee08e6ad05e332026a42178a42b2cf04384b0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,5 +11,5 @@ reqwest = "0.11.22"
 url = "2.5.0"
 
 [dev-dependencies]
-tokio = { version = "1.34.0", default-features = false, features = ["rt", "rt-multi-thread"]}
+tokio = { version = "1.34.0", default-features = false, features = ["rt", "rt-multi-thread", "macros"]}
 dotenv = "*"
diff --git a/README.md b/README.md
index 22967caf229ac9957cc69e8f6f5a690fb1c326c7..87bccb2c5a508cc421c3cd65e786f2ac40f85371 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# mquery (placeholder name)
+# mquery
 A Rust library for handling PROMQL (and possibly MetricsQL) queries.
 
 ## Usage Example
@@ -11,8 +11,7 @@ async fn main() {
     let url = std::env::var("VM_URL").expect("VM URL not found in env");
     let token = std::env::var("VM_TOKEN").expect("VM token not found in env");
 
-    let mut query = query::Metric::new("total_http_requests");
-    query.label_eq("method", "get");
+    let query = query::Metric::new("total_http_requests").label_eq("method", "get");
 
     let _response = QueryManager::new(url.parse().unwrap())
                       .auth(Auth::Bearer(token))
@@ -21,6 +20,8 @@ async fn main() {
                       .expect("operation failed");
 }
 ```
+# Testing
+Requires a local Victoria Metrics server running at `http://127.0.0.1:8428/` for the integration tests to work.
 
 # Roadmap
 - [x] Basic raw queries (Instant and Ranged)
diff --git a/src/lib.rs b/src/lib.rs
index 1bb88c7509f6509fcaa57c7df77d6f3040e7f4e3..67dcae191bedd1eafbdc1c2b2604e6759bf20fe5 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -131,3 +131,7 @@ pub enum Method {
     Get,
     Post,
 }
+
+pub(crate) mod seal {
+    pub(crate) trait Sealed {}
+}
diff --git a/src/query.rs b/src/query/mod.rs
similarity index 85%
rename from src/query.rs
rename to src/query/mod.rs
index da1a63fd06224b5007a0cc70e8ff2ddbf5b35d51..24ce3866129bcd531d595c083f4e5dd38259b3c2 100644
--- a/src/query.rs
+++ b/src/query/mod.rs
@@ -1,5 +1,9 @@
 use std::fmt::Display;
 
+use crate::seal::Sealed;
+
+pub mod ops;
+
 // Add default implementation of IntoQuery where the type simply returns itself
 // Used for types that already implement AsRef<str>
 macro_rules! impl_into_query {
@@ -56,7 +60,7 @@ impl<'a> Metric<'a> {
     /// Add a label matcher with an equality operator i.e. name="value"
     ///
     /// For addding multiple labels at once, see [`Self::labels`]
-    pub fn label_eq(&mut self, name: &'a str, value: &'a str) -> &mut Self {
+    pub fn label_eq(mut self, name: &'a str, value: &'a str) -> Self {
         self.labels.push(Label::Eq(name, value));
         self
     }
@@ -64,7 +68,7 @@ impl<'a> Metric<'a> {
     /// Add a label matcher with an inequality operator i.e. name!="value"
     ///
     /// For addding multiple labels at once, see [`Self::labels`]
-    pub fn label_ne(&mut self, name: &'a str, value: &'a str) -> &mut Self {
+    pub fn label_ne(mut self, name: &'a str, value: &'a str) -> Self {
         self.labels.push(Label::Ne(name, value));
         self
     }
@@ -72,7 +76,7 @@ impl<'a> Metric<'a> {
     /// Add a regular expression label matcher with an equality operator i.e. name=~".+"
     ///
     /// For addding multiple labels at once, see [`Self::labels`]
-    pub fn label_rx_eq(&mut self, name: &'a str, value: &'a str) -> &mut Self {
+    pub fn label_rx_eq(mut self, name: &'a str, value: &'a str) -> Self {
         self.labels.push(Label::RxEq(name, value));
         self
     }
@@ -80,7 +84,7 @@ impl<'a> Metric<'a> {
     /// Add a regular expression label matcher with an equality operator i.e. name!~".+"
     ///
     /// For addding multiple labels at once, see [`Self::labels`]
-    pub fn label_rx_ne(&mut self, name: &'a str, value: &'a str) -> &mut Self {
+    pub fn label_rx_ne(mut self, name: &'a str, value: &'a str) -> Self {
         self.labels.push(Label::RxNe(name, value));
         self
     }
@@ -98,13 +102,13 @@ impl<'a> Metric<'a> {
     ///         Label::RxEq("name", "value"),
     ///     ]);
     /// ```
-    pub fn labels<I: IntoIterator<Item = Label<'a>>>(&mut self, labels: I) -> &mut Self {
+    pub fn labels<I: IntoIterator<Item = Label<'a>>>(mut self, labels: I) -> Self {
         self.labels.extend(labels);
         self
     }
 
     /// Set query evaluation time to the specified Unix timestamp using the `@` modifier.
-    pub fn at(&mut self, timestamp: i32) -> &mut Self {
+    pub fn at(mut self, timestamp: i32) -> Self {
         // TODO: Convert between chrono DataTime
         self.at.replace(timestamp);
         self
@@ -113,7 +117,7 @@ impl<'a> Metric<'a> {
     /// Set lookback duration / range for the query with the appropriate unit of time
     ///
     /// More info on range selectors can be found at [Range Vector Selectors](https://prometheus.io/docs/prometheus/latest/querying/basics/#range-vector-selectors)
-    pub fn lookback(&mut self, duration: Duration) -> &mut Self {
+    pub fn lookback(mut self, duration: Duration) -> Self {
         self.lookback.replace(duration);
         self
     }
@@ -121,7 +125,7 @@ impl<'a> Metric<'a> {
     /// Set the offset time fromt the query evaluation time using the offset modifier with the appropriate unit
     ///
     /// Note: Offsets can contain [negative values](https://prometheus.io/docs/prometheus/latest/querying/basics/#offset-modifier)
-    pub fn offset(&mut self, offset: Offset) -> &mut Self {
+    pub fn offset(mut self, offset: Offset) -> Self {
         self.offset.replace(offset);
         self
     }
@@ -161,6 +165,9 @@ impl IntoQuery for Metric<'_> {
     }
 }
 
+impl Sealed for Metric<'_> {}
+impl ops::Operable for Metric<'_> {}
+
 /// Label matcher type with (Name, Value) pairs
 #[derive(Debug, Clone)]
 pub enum Label<'a> {
@@ -228,6 +235,23 @@ impl Display for Offset {
     }
 }
 
+/// A scalar value that represents and also holds double precision floating point numbers
+///
+/// This is one of the two base types for building typed queries. Another is [``Metric``].
+/// Scalar implements [``Operable``], which means that it supports the operations enabled by the
+/// [``ops``] module.
+#[derive(Debug, Clone, Copy, Default)]
+pub struct Scalar(pub f64);
+
+impl Display for Scalar {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl Sealed for Scalar {}
+impl ops::Operable for Scalar {}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/src/query/ops/arith.rs b/src/query/ops/arith.rs
new file mode 100644
index 0000000000000000000000000000000000000000..47bdef877bd49e8511a7a3b7c8de3f7da8c44025
--- /dev/null
+++ b/src/query/ops/arith.rs
@@ -0,0 +1,268 @@
+use std::fmt::Display;
+
+use crate::{query::IntoQuery, seal::Sealed};
+
+use super::{
+    modifiers::{GroupType, MatchType, Mod},
+    Operable,
+};
+
+/// Trait that enables arithmetic query operations on all types that implement [``Operable``]
+/// (i.e. [``super::Scalar``] and [``super::Metric``] )
+pub trait Arithmetic<'a>: Operable
+where
+    Self: Sized,
+{
+    /// Add `Self` with another operable type to produce a query that looks like `(self + right)`
+    fn add<R: Operable>(self, right: R) -> ArithmeticOperation<'a, Self, R> {
+        ArithmeticOperation::new(ArthOp::Add, self, right)
+    }
+    /// Subtract `Self` with another operable type to produce a query that looks like `(self - right)`
+    fn sub<R: Operable>(self, right: R) -> ArithmeticOperation<'a, Self, R> {
+        ArithmeticOperation::new(ArthOp::Sub, self, right)
+    }
+    /// Multiply `Self` with another operable type to produce a query that looks like `(self * right)`
+    fn mul<R: Operable>(self, right: R) -> ArithmeticOperation<'a, Self, R> {
+        ArithmeticOperation::new(ArthOp::Mul, self, right)
+    }
+    /// Divide `Self` with another operable type to produce a query that looks like `(self / right)`
+    fn div<R: Operable>(self, right: R) -> ArithmeticOperation<'a, Self, R> {
+        ArithmeticOperation::new(ArthOp::Div, self, right)
+    }
+    /// Apply modulo operator between `Self` and another operable type to produce a query that looks like `(self % right)`
+    fn modulo<R: Operable>(self, right: R) -> ArithmeticOperation<'a, Self, R> {
+        ArithmeticOperation::new(ArthOp::Mod, self, right)
+    }
+
+    /// Apply the exponent/power operator between `Self` and another operable type to produce a query that looks like `(self ^ right)`
+    fn pow<R: Operable>(self, right: R) -> ArithmeticOperation<'a, Self, R> {
+        ArithmeticOperation::new(ArthOp::Pow, self, right)
+    }
+}
+
+impl<T> Arithmetic<'_> for T where T: Operable {}
+
+/// Represents an arithmetic operation between type `A` and type `B`
+/// where `A` and `B` both implement [``Operable``]
+///
+/// This struct is not meant to be constructed directly by the user of the library.
+/// Queries with arithmetic operations should be constructed through the [``Arithmetic``] trait.
+#[derive(Debug, Clone)]
+pub struct ArithmeticOperation<'a, A, B>
+where
+    A: Operable,
+    B: Operable,
+{
+    op_type: ArthOp,
+    left: A,
+    right: B,
+    matching: Mod<'a, MatchType>,
+    group_mod: Mod<'a, GroupType>,
+}
+
+impl<'a, A, B> ArithmeticOperation<'a, A, B>
+where
+    A: Operable,
+    B: Operable,
+{
+    fn new(op_type: ArthOp, left: A, right: B) -> Self {
+        Self {
+            op_type,
+            left,
+            right,
+            matching: Default::default(),
+            group_mod: Default::default(),
+        }
+    }
+    /// Apply `group_left` group modifier to the arithmetic operation without specifying any label names
+    pub fn group_left(mut self) -> Self {
+        self.group_mod = Mod::from((GroupType::Left, []));
+        self
+    }
+    /// Apply `group_left` group modifier to the arithmetic operation with label names
+    pub fn group_left_labels(mut self, labels: impl IntoIterator<Item = &'a str>) -> Self {
+        self.group_mod = Mod::from((GroupType::Left, labels));
+        self
+    }
+    /// Apply `group_right` group modifier to the arithmetic operation without specifying any label names
+    pub fn group_right(mut self) -> Self {
+        self.group_mod = Mod::from((GroupType::Right, []));
+        self
+    }
+    /// Apply `group_right` group modifier to the arithmetic operation with label names
+    pub fn group_right_labels(mut self, labels: impl IntoIterator<Item = &'a str>) -> Self {
+        self.group_mod = Mod::from((GroupType::Right, labels));
+        self
+    }
+    /// Match only given labels between the two vectors for evauluating the arithmetic operation
+    ///
+    /// Note: Only to be used with vector expressions on both left and right
+    /// i.e. `<vector expr> <bin-op> on(<label list>) <vector expr>`
+    pub fn match_on(mut self, labels: impl IntoIterator<Item = &'a str>) -> Self {
+        self.matching = Mod::from((MatchType::On, labels));
+        self
+    }
+
+    /// Ignore given labels between the two vectors for evauluating the arithmetic operation
+    ///
+    /// Note: Only to be used with vector expressions on both left and right
+    /// i.e. `<vector expr> <bin-op> ignoring(<label list>) <vector expr>`
+    pub fn ignoring(mut self, labels: impl IntoIterator<Item = &'a str>) -> Self {
+        self.matching = Mod::from((MatchType::Ignoring, labels));
+        self
+    }
+}
+
+impl<A, B> Operable for ArithmeticOperation<'_, A, B>
+where
+    A: Operable,
+    B: Operable,
+{
+}
+
+impl<A, B> Sealed for ArithmeticOperation<'_, A, B>
+where
+    A: Operable,
+    B: Operable,
+{
+}
+
+#[derive(Clone, Copy, Debug)]
+enum ArthOp {
+    Add = '+' as isize,
+    Sub = '-' as isize,
+    Mul = '*' as isize,
+    Div = '/' as isize,
+    Mod = '%' as isize,
+    Pow = '^' as isize,
+}
+
+impl ArthOp {
+    fn to_char(self) -> char {
+        self as u8 as char
+    }
+}
+
+impl<A, B> Display for ArithmeticOperation<'_, A, B>
+where
+    A: Operable,
+    B: Operable,
+{
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let op = self.op_type.to_char();
+        write!(
+            f,
+            "({left} {op}{matching}{group} {right})",
+            left = self.left,
+            right = self.right,
+            matching = self.matching,
+            group = self.group_mod,
+        )
+    }
+}
+
+impl<A, B> IntoQuery for ArithmeticOperation<'_, A, B>
+where
+    A: Operable,
+    B: Operable,
+{
+    type Target = String;
+    fn into_query(self) -> Self::Target {
+        self.to_string()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::query::{Metric, Scalar};
+
+    #[test]
+    fn arith_all_ops() {
+        let query = Scalar(1.0);
+        assert_eq!(query.add(query).to_string(), "(1 + 1)");
+        assert_eq!(query.sub(query).to_string(), "(1 - 1)");
+        assert_eq!(query.mul(query).to_string(), "(1 * 1)");
+        assert_eq!(query.div(query).to_string(), "(1 / 1)");
+        assert_eq!(query.modulo(query).to_string(), "(1 % 1)");
+        assert_eq!(query.pow(query).to_string(), "(1 ^ 1)");
+    }
+
+    #[test]
+    fn arith_scalar_scalar() {
+        let query = Scalar(0.0).add(Scalar(1.0));
+        assert_eq!(query.to_string(), "(0 + 1)");
+    }
+
+    #[test]
+    fn arith_metric_scalar() {
+        let query = Metric::new("test_metric").sub(Scalar(0.5));
+        assert_eq!(query.to_string(), "(test_metric - 0.5)");
+    }
+
+    #[test]
+    fn arith_nested() {
+        let query = Metric::new("test_metric").sub(Scalar(0.5)).mul(Scalar(0.1));
+        assert_eq!(query.to_string(), "((test_metric - 0.5) * 0.1)");
+    }
+
+    #[test]
+    fn arith_match_on() {
+        let metric = Metric::new("test_metric")
+            .add(Metric::new("another_metric"))
+            .match_on(["one", "two"])
+            .to_string();
+        assert_eq!(metric, "(test_metric + on(one,two) another_metric)");
+    }
+
+    #[test]
+    fn arith_match_ignoring() {
+        let metric = Metric::new("test_metric")
+            .add(Metric::new("another_metric"))
+            .ignoring(["one", "two"])
+            .to_string();
+        assert_eq!(metric, "(test_metric + ignoring(one,two) another_metric)");
+    }
+
+    #[test]
+    fn arith_group_left() {
+        let metric = Metric::new("test_metric")
+            .add(Metric::new("another_metric"))
+            .group_left()
+            .to_string();
+        assert_eq!(metric, "(test_metric + group_left another_metric)");
+    }
+
+    #[test]
+    fn arith_group_right() {
+        let metric = Metric::new("test_metric")
+            .add(Metric::new("another_metric"))
+            .group_right()
+            .to_string();
+        assert_eq!(metric, "(test_metric + group_right another_metric)");
+    }
+
+    #[test]
+    fn arith_group_left_labels() {
+        let metric = Metric::new("test_metric")
+            .add(Metric::new("another_metric"))
+            .group_left_labels(["label", "label2"])
+            .to_string();
+        assert_eq!(
+            metric,
+            "(test_metric + group_left(label,label2) another_metric)"
+        );
+    }
+
+    #[test]
+    fn arith_group_right_labels() {
+        let metric = Metric::new("test_metric")
+            .add(Metric::new("another_metric"))
+            .group_right_labels(["label", "label2"])
+            .to_string();
+        assert_eq!(
+            metric,
+            "(test_metric + group_right(label,label2) another_metric)"
+        );
+    }
+}
diff --git a/src/query/ops/cmp.rs b/src/query/ops/cmp.rs
new file mode 100644
index 0000000000000000000000000000000000000000..bc5126df2c28610724972117248ec979e20617db
--- /dev/null
+++ b/src/query/ops/cmp.rs
@@ -0,0 +1,329 @@
+use super::{
+    modifiers::{GroupType, MatchType, Mod},
+    Operable,
+};
+use crate::{query::IntoQuery, seal::Sealed};
+use std::fmt::Display;
+
+/// Represents a Comparison operation between type `A` and type `B`
+/// where `A` and `B` both implement [``Operable``]
+///
+/// This struct is not meant to be constructed directly by the user of the library.
+/// Queries with arithmetic operations should be constructed through the [``Comparison``] trait.
+#[derive(Debug, Clone)]
+pub struct CompOperation<'a, A, B>
+where
+    A: Operable,
+    B: Operable,
+{
+    cmp_type: CmpOp,
+    bool: bool,
+    left: A,
+    right: B,
+    matching: Mod<'a, MatchType>,
+    group_mod: Mod<'a, GroupType>,
+}
+
+impl<'a, A, B> CompOperation<'a, A, B>
+where
+    A: Operable,
+    B: Operable,
+{
+    fn new(cmp_type: CmpOp, bool: bool, left: A, right: B) -> Self {
+        Self {
+            cmp_type,
+            left,
+            right,
+            bool,
+            matching: Default::default(),
+            group_mod: Default::default(),
+        }
+    }
+
+    /// Apply `group_left` group modifier to the comparison operation without specifying any label names
+    pub fn group_left(mut self) -> Self {
+        self.group_mod = Mod::from((GroupType::Left, []));
+        self
+    }
+
+    /// Apply `group_left` group modifier to the comparison operation with label names
+    pub fn group_left_labels(mut self, labels: impl IntoIterator<Item = &'a str>) -> Self {
+        self.group_mod = Mod::from((GroupType::Left, labels));
+        self
+    }
+
+    /// Apply `group_right` group modifier to the comparison operation without specifying any label names
+    pub fn group_right(mut self) -> Self {
+        self.group_mod = Mod::from((GroupType::Right, []));
+        self
+    }
+
+    /// Apply `group_right` group modifier to the comparison operation with label names
+    pub fn group_right_labels(mut self, labels: impl IntoIterator<Item = &'a str>) -> Self {
+        self.group_mod = Mod::from((GroupType::Right, labels));
+        self
+    }
+
+    /// Match only given labels between the two vectors for evauluating the comparison operation
+    ///
+    /// Note: Only to be used with vector expressions on both left and right
+    /// i.e. `<vector expr> <bin-op> on(<label list>) <vector expr>`
+    pub fn match_on(mut self, labels: impl IntoIterator<Item = &'a str>) -> Self {
+        self.matching = Mod::from((MatchType::On, labels));
+        self
+    }
+
+    /// Match only given labels between the two vectors for evauluating the comparison operation
+    ///
+    /// Note: Only to be used with vector expressions on both left and right
+    /// i.e. `<vector expr> <bin-op> ignoring(<label list>) <vector expr>`
+    pub fn ignoring(mut self, labels: impl IntoIterator<Item = &'a str>) -> Self {
+        self.matching = Mod::from((MatchType::Ignoring, labels));
+        self
+    }
+}
+
+impl<A, B> Display for CompOperation<'_, A, B>
+where
+    A: Operable,
+    B: Operable,
+{
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let b = self.bool.then_some(" bool").unwrap_or_default();
+        write!(
+            f,
+            "({left} {op}{b}{matching}{group} {right})",
+            left = self.left,
+            right = self.right,
+            op = self.cmp_type.to_str(),
+            matching = self.matching,
+            group = self.group_mod,
+        )
+    }
+}
+
+impl<A, B> Operable for CompOperation<'_, A, B>
+where
+    A: Operable,
+    B: Operable,
+{
+}
+
+impl<A, B> Sealed for CompOperation<'_, A, B>
+where
+    A: Operable,
+    B: Operable,
+{
+}
+
+impl<A, B> IntoQuery for CompOperation<'_, A, B>
+where
+    A: Operable,
+    B: Operable,
+{
+    type Target = String;
+    fn into_query(self) -> Self::Target {
+        self.to_string()
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+enum CmpOp {
+    Eq,
+    Ne,
+    Gt,
+    Lt,
+    Gteq,
+    Lteq,
+}
+
+impl CmpOp {
+    fn to_str(self) -> &'static str {
+        match self {
+            CmpOp::Eq => "==",
+            CmpOp::Ne => "!=",
+            CmpOp::Gt => ">",
+            CmpOp::Lt => "<",
+            CmpOp::Gteq => ">=",
+            CmpOp::Lteq => "<=",
+        }
+    }
+}
+
+/// Trait to enable comparison query operators on all types that implement [``Operable``]
+pub trait Comparison<'a>: Operable
+where
+    Self: Sized,
+{
+    /// Apply the equality or `==` comparison operator to `self` and another operable type
+    ///
+    /// The resulting query will have a format of `(self == right)`
+    fn cmp_eq<R: Operable>(self, right: R) -> CompOperation<'a, Self, R> {
+        CompOperation::new(CmpOp::Eq, false, self, right)
+    }
+
+    /// Apply the equality or `==` comparison operator to `self` and another operable type with the bool modifier
+    ///
+    /// The resulting query will have a format of `(self == bool right)`
+    fn cmp_eq_bool<R: Operable>(self, right: R) -> CompOperation<'a, Self, R> {
+        CompOperation::new(CmpOp::Eq, true, self, right)
+    }
+
+    /// Apply the inequality or `!=` comparison operator to `self` and another operable type
+    ///
+    /// The resulting query will have a format of `(self == right)`
+    fn cmp_ne<R: Operable>(self, right: R) -> CompOperation<'a, Self, R> {
+        CompOperation::new(CmpOp::Ne, false, self, right)
+    }
+
+    /// Apply the inequality or `!=` comparison operator to `self` and another operable type with the bool modifier
+    ///
+    /// The resulting query will have a format of `(self != bool right)`
+    fn cmp_ne_bool<R: Operable>(self, right: R) -> CompOperation<'a, Self, R> {
+        CompOperation::new(CmpOp::Ne, true, self, right)
+    }
+
+    /// Apply the greater-than or `>` comparison operator to `self` and another operable type
+    ///
+    /// The resulting query will have a format of `(self > right)`
+    fn cmp_gt<R: Operable>(self, right: R) -> CompOperation<'a, Self, R> {
+        CompOperation::new(CmpOp::Gt, false, self, right)
+    }
+
+    /// Apply the greater-than or `>` comparison operator to `self` and another operable type with the bool modifier
+    ///
+    /// The resulting query will have a format of `(self > bool right)`
+    fn cmp_gt_bool<R: Operable>(self, right: R) -> CompOperation<'a, Self, R> {
+        CompOperation::new(CmpOp::Gt, true, self, right)
+    }
+
+    /// Apply the less-than or `<` comparison operator to `self` and another operable type
+    ///
+    /// The resulting query will have a format of `(self < right)`
+    fn cmp_lt<R: Operable>(self, right: R) -> CompOperation<'a, Self, R> {
+        CompOperation::new(CmpOp::Lt, false, self, right)
+    }
+
+    /// Apply the less-than or `<` comparison operator to `self` and another operable type with the bool modifier
+    ///
+    /// The resulting query will have a format of `(self < bool right)`
+    fn cmp_lt_bool<R: Operable>(self, right: R) -> CompOperation<'a, Self, R> {
+        CompOperation::new(CmpOp::Lt, true, self, right)
+    }
+
+    /// Apply the greater-than/equal or `>=` comparison operator to `self` and another operable type with the bool modifier
+    ///
+    /// The resulting query will have a format of `(self >= right)`
+    fn cmp_gteq<R: Operable>(self, right: R) -> CompOperation<'a, Self, R> {
+        CompOperation::new(CmpOp::Gteq, false, self, right)
+    }
+
+    /// Apply the greater-than/equal or `>=` comparison operator to `self` and another operable type with the bool modifier
+    ///
+    /// The resulting query will have a format of `(self >= bool right)`
+    fn cmp_gteq_bool<R: Operable>(self, right: R) -> CompOperation<'a, Self, R> {
+        CompOperation::new(CmpOp::Gteq, true, self, right)
+    }
+
+    /// Apply the less-than/equal or `<=` comparison operator to `self` and another operable type
+    ///
+    /// The resulting query will have a format of `(self <= bool right)`
+    fn cmp_lteq<R: Operable>(self, right: R) -> CompOperation<'a, Self, R> {
+        CompOperation::new(CmpOp::Lteq, false, self, right)
+    }
+
+    /// Apply the less-than/equal or `<=` comparison operator to `self` and another operable type with the bool modifier
+    ///
+    /// The resulting query will have a format of `(self <= bool right)`
+    fn cmp_lteq_bool<R: Operable>(self, right: R) -> CompOperation<'a, Self, R> {
+        CompOperation::new(CmpOp::Lteq, true, self, right)
+    }
+}
+
+impl<T> Comparison<'_> for T where T: Operable {}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::query::{Metric, Scalar};
+
+    #[test]
+    fn cmp_all_ops() {
+        let query = Scalar(1.0);
+        assert_eq!(query.cmp_eq(query).to_string(), "(1 == 1)");
+        assert_eq!(query.cmp_eq_bool(query).to_string(), "(1 == bool 1)");
+        assert_eq!(query.cmp_ne(query).to_string(), "(1 != 1)");
+        assert_eq!(query.cmp_ne_bool(query).to_string(), "(1 != bool 1)");
+        assert_eq!(query.cmp_gt(query).to_string(), "(1 > 1)");
+        assert_eq!(query.cmp_gt_bool(query).to_string(), "(1 > bool 1)");
+        assert_eq!(query.cmp_lt(query).to_string(), "(1 < 1)");
+        assert_eq!(query.cmp_lt_bool(query).to_string(), "(1 < bool 1)");
+        assert_eq!(query.cmp_gteq(query).to_string(), "(1 >= 1)");
+        assert_eq!(query.cmp_gteq_bool(query).to_string(), "(1 >= bool 1)");
+        assert_eq!(query.cmp_lteq(query).to_string(), "(1 <= 1)");
+        assert_eq!(query.cmp_lteq_bool(query).to_string(), "(1 <= bool 1)");
+    }
+
+    #[test]
+    fn cmp_scalar_scalar() {
+        let query = Scalar(0.0).cmp_eq(Scalar(1.0));
+        assert_eq!(query.to_string(), "(0 == 1)");
+    }
+
+    #[test]
+    fn cmp_metric_scalar() {
+        let query = Metric::new("test_metric").cmp_ne(Scalar(0.5));
+        assert_eq!(query.to_string(), "(test_metric != 0.5)");
+    }
+
+    #[test]
+    fn cmp_nested() {
+        let query = Metric::new("test_metric")
+            .cmp_eq(Scalar(0.5))
+            .cmp_gteq(Scalar(0.1));
+
+        assert_eq!(query.to_string(), "((test_metric == 0.5) >= 0.1)");
+    }
+
+    #[test]
+    fn cmp_group_left() {
+        let metric = Metric::new("test_metric")
+            .cmp_eq(Metric::new("another_metric"))
+            .group_left()
+            .to_string();
+        assert_eq!(metric, "(test_metric == group_left another_metric)");
+    }
+
+    #[test]
+    fn cmp_group_right() {
+        let metric = Metric::new("test_metric")
+            .cmp_eq(Metric::new("another_metric"))
+            .group_right()
+            .to_string();
+        assert_eq!(metric, "(test_metric == group_right another_metric)");
+    }
+
+    #[test]
+    fn cmp_group_left_labels() {
+        let metric = Metric::new("test_metric")
+            .cmp_eq(Metric::new("another_metric"))
+            .group_left_labels(["label", "label2"])
+            .to_string();
+        assert_eq!(
+            metric,
+            "(test_metric == group_left(label,label2) another_metric)"
+        );
+    }
+
+    #[test]
+    fn cmp_group_right_labels() {
+        let metric = Metric::new("test_metric")
+            .cmp_eq(Metric::new("another_metric"))
+            .group_right_labels(["label", "label2"])
+            .to_string();
+        assert_eq!(
+            metric,
+            "(test_metric == group_right(label,label2) another_metric)"
+        );
+    }
+}
diff --git a/src/query/ops/logic.rs b/src/query/ops/logic.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d42f8d57740d6a44cd5ba0ec46fc1e0f7f94c375
--- /dev/null
+++ b/src/query/ops/logic.rs
@@ -0,0 +1,169 @@
+use super::{
+    arith::ArithmeticOperation,
+    cmp::CompOperation,
+    modifiers::{MatchType, Mod},
+    Operable,
+};
+use crate::{
+    query::{IntoQuery, Metric},
+    seal::Sealed,
+};
+use std::fmt::Display;
+
+/// Represents a logical/set query operation (i.e. and, or, unless).
+///
+/// This type is meant to be only constructed through the provided methods on the
+/// [``Metric``] type.
+#[derive(Debug, Clone)]
+pub struct LogicalOperation<'a, A: Operable, B: Operable> {
+    op_type: LogicOp,
+    left: A,
+    right: B,
+    matching: Mod<'a, MatchType>,
+}
+
+impl<'a, A: Operable, B: Operable> LogicalOperation<'a, A, B> {
+    /// Match only given labels between the two vectors for evauluating the logical/set operation
+    ///
+    /// Note: Only to be used with vector expressions on both left and right
+    /// i.e. `<vector expr> <bin-op> on(<label list>) <vector expr>`
+    pub fn match_on(mut self, labels: impl IntoIterator<Item = &'a str>) -> Self {
+        self.matching = Mod::from((MatchType::On, labels));
+        self
+    }
+
+    /// Ignore given labels between the two vectors for evauluating the logical/set operation
+    ///
+    /// Note: Only to be used with vector expressions on both left and right
+    /// i.e. `<vector expr> <bin-op> ignoring(<label list>) <vector expr>`
+    pub fn ignoring(mut self, labels: impl IntoIterator<Item = &'a str>) -> Self {
+        self.matching = Mod::from((MatchType::Ignoring, labels));
+        self
+    }
+}
+
+impl<A: Operable, B: Operable> Display for LogicalOperation<'_, A, B> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "({a} {op}{matching} {b})",
+            a = self.left,
+            b = self.right,
+            op = self.op_type,
+            matching = self.matching,
+        )
+    }
+}
+
+impl<A: Operable, B: Operable> Operable for LogicalOperation<'_, A, B> {}
+impl<A: Operable, B: Operable> Sealed for LogicalOperation<'_, A, B> {}
+
+impl<A: Operable, B: Operable> IntoQuery for LogicalOperation<'_, A, B> {
+    type Target = String;
+    fn into_query(self) -> Self::Target {
+        self.to_string()
+    }
+}
+
+#[derive(Clone, Copy, Debug)]
+enum LogicOp {
+    And,
+    Or,
+    Unless,
+}
+
+impl Display for LogicOp {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(match self {
+            LogicOp::And => "and",
+            LogicOp::Or => "or",
+            LogicOp::Unless => "unless",
+        })
+    }
+}
+
+/// The trait that enables logical/set operations between two instant vectors
+///
+/// > Caution: Unlike other operator traits, this trait doesn't contain a blanket implementation for all types that implement [``Operable``].
+/// > This is to prevent the [``crate::query::Scalar``] being used as an operand.
+/// > Care must be taken when using these operators with complex queries that might evaluate to scalar values instead of instant vectors.
+pub trait Logical<'a>: Operable
+where
+    Self: Sized,
+{
+    /// Apply the `and` logical binary operator to `Self` and other [``Operable``] type (note: Scalar type is not supported)
+    ///
+    /// The resulting query would look like `(self and right)`
+    fn and<R: Operable>(self, right: R) -> LogicalOperation<'a, Self, R> {
+        LogicalOperation {
+            op_type: LogicOp::And,
+            left: self,
+            right,
+            matching: Mod::default(),
+        }
+    }
+
+    /// Apply the `or` logical binary operator to `Self` and other [``Operable``] type (note: Scalar type is not supported)
+    ///
+    /// The resulting query would look like `(self or right)`
+    fn or<R: Operable>(self, right: R) -> LogicalOperation<'a, Self, R> {
+        LogicalOperation {
+            op_type: LogicOp::Or,
+            left: self,
+            right,
+            matching: Mod::default(),
+        }
+    }
+
+    /// Apply the `unless` logical binary operator to `Self` and other [``Operable``] type (note: Scalar type is not supported)
+    ///
+    /// The resulting query would look like `(self unless right)`
+    fn unless<R: Operable>(self, right: R) -> LogicalOperation<'a, Self, R> {
+        LogicalOperation {
+            op_type: LogicOp::Unless,
+            left: self,
+            right,
+            matching: Mod::default(),
+        }
+    }
+}
+
+impl Logical<'_> for Metric<'_> {}
+impl<A: Operable, B: Operable> Logical<'_> for LogicalOperation<'_, A, B> {}
+impl<A: Operable, B: Operable> Logical<'_> for ArithmeticOperation<'_, A, B> {}
+impl<A: Operable, B: Operable> Logical<'_> for CompOperation<'_, A, B> {}
+
+#[cfg(test)]
+mod tests {
+    use crate::query::{ops::logic::Logical, Metric};
+
+    #[test]
+    fn logic_all_ops() {
+        let query = Metric::new("one").and(Metric::new("two")).to_string();
+        assert_eq!(query, "(one and two)");
+
+        let query = Metric::new("one").or(Metric::new("two")).to_string();
+        assert_eq!(query, "(one or two)");
+
+        let query = Metric::new("one").unless(Metric::new("two")).to_string();
+        assert_eq!(query, "(one unless two)");
+    }
+
+    #[test]
+    fn logic_match_on() {
+        let metric = Metric::new("test_metric")
+            .and(Metric::new("another_metric"))
+            .match_on(["one", "two"])
+            .to_string();
+        assert_eq!(metric, "(test_metric and on(one,two) another_metric)");
+    }
+
+    #[test]
+    fn logic_match_ignoring() {
+        let metric = Metric::new("test_metric")
+            .and(Metric::new("another_metric"))
+            .ignoring(["one", "two"])
+            .to_string();
+        assert_eq!(metric, "(test_metric and ignoring(one,two) another_metric)");
+    }
+}
diff --git a/src/query/ops/mod.rs b/src/query/ops/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c943ee2340525f02d73b3956307bbc2394b9353c
--- /dev/null
+++ b/src/query/ops/mod.rs
@@ -0,0 +1,34 @@
+//! All operator related types and traits for building queries
+//!
+//! [``Arithmetic``], [``Comparison``] and [``Logical``] traits can be brought into scope to enable
+//! binary operators of the respective type on the base query types such as
+//! [``super::Metric``] and [``super::Scalar``].
+//!
+//! # Example
+//! ```
+//! use mquery::query::{
+//!     ops::{Arithmetic, Comparison},
+//!     Metric, Scalar,
+//! };
+//!
+//! let _ = Metric::new("target_metric")
+//!     .add(Metric::new("second_metric"))
+//!     .mul(Scalar(0.5))
+//!     .cmp_eq_bool(Metric::new("third_metric"));
+//! ```
+
+use crate::seal::Sealed;
+use std::fmt::Display;
+
+mod arith;
+mod cmp;
+mod logic;
+pub(crate) mod modifiers;
+
+pub use arith::Arithmetic;
+pub use cmp::Comparison;
+pub use logic::Logical;
+
+/// Marker trait for signifying that a type can be used with the operators
+#[allow(private_bounds)]
+pub trait Operable: Display + Sealed {}
diff --git a/src/query/ops/modifiers.rs b/src/query/ops/modifiers.rs
new file mode 100644
index 0000000000000000000000000000000000000000..df5b30edee268db35601acf5aa501c789a7d678c
--- /dev/null
+++ b/src/query/ops/modifiers.rs
@@ -0,0 +1,93 @@
+use std::fmt::Display;
+
+#[derive(Debug, Clone, Default)]
+pub(crate) struct Mod<'a, M> {
+    mod_type: M,
+    labels: Vec<&'a str>,
+}
+
+impl<'a, T: IntoIterator<Item = &'a str>> From<(MatchType, T)> for Mod<'a, MatchType> {
+    fn from((match_type, labels): (MatchType, T)) -> Self {
+        Mod {
+            mod_type: match_type,
+            labels: labels.into_iter().collect(),
+        }
+    }
+}
+
+impl<'a, T: IntoIterator<Item = &'a str>> From<(GroupType, T)> for Mod<'a, GroupType> {
+    fn from((grp_type, labels): (GroupType, T)) -> Self {
+        Mod {
+            mod_type: grp_type,
+            labels: labels.into_iter().collect(),
+        }
+    }
+}
+
+impl Display for Mod<'_, MatchType> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        if !self.labels.is_empty() && !matches!(self.mod_type, MatchType::None) {
+            write!(
+                f,
+                " {matchtype}({labels})",
+                matchtype = self.mod_type,
+                labels = self.labels.join(",")
+            )?;
+        }
+        Ok(())
+    }
+}
+
+impl Display for Mod<'_, GroupType> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        if !matches!(self.mod_type, GroupType::None) {
+            if self.labels.is_empty() {
+                return write!(f, " {grp_type}", grp_type = self.mod_type,);
+            }
+
+            return write!(
+                f,
+                " {grp_type}({labels})",
+                grp_type = self.mod_type,
+                labels = self.labels.join(","),
+            );
+        }
+        Ok(())
+    }
+}
+
+#[derive(Default, Debug, Clone, Copy)]
+pub(crate) enum MatchType {
+    On,
+    Ignoring,
+    #[default]
+    None,
+}
+
+impl Display for MatchType {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(match self {
+            MatchType::On => "on",
+            MatchType::Ignoring => "ignoring",
+            MatchType::None => "",
+        })
+    }
+}
+
+#[derive(Debug, Clone, Copy, Default)]
+pub(crate) enum GroupType {
+    Left,
+    Right,
+    #[default]
+    None,
+}
+
+impl Display for GroupType {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(match self {
+            GroupType::Left => "group_left",
+            GroupType::Right => "group_right",
+            GroupType::None => "",
+        })
+    }
+}
diff --git a/tests/operators.rs b/tests/operators.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a27ce155e8ed9b0822264b2a84d99a1a18b3d825
--- /dev/null
+++ b/tests/operators.rs
@@ -0,0 +1,91 @@
+use mquery::query::{
+    ops::{Arithmetic, Comparison, Logical},
+    Metric, Scalar,
+};
+
+mod utils;
+
+// Arbitrary queries to test whether they produce valid syntax
+// The server will return an error if there is a syntax error and the test will fail
+
+#[tokio::test]
+async fn arithmetic() {
+    let query = Metric::new("test_metric")
+        .label_eq("label", "value")
+        .add(Metric::new("another_metric").label_ne("label", "value"))
+        .mul(Scalar(0.1))
+        .sub(Metric::new("third_metric"))
+        .div(Scalar(0.5))
+        .pow(Scalar(2.5));
+    utils::send_query(query).await.unwrap();
+}
+
+#[tokio::test]
+async fn arithmetic_match_on() {
+    let query = Metric::new("test_metric")
+        .label_eq("label", "value")
+        .add(Metric::new("another_metric").label_ne("label", "value"))
+        .mul(Scalar(0.1))
+        .sub(Metric::new("third_metric"))
+        .div(Scalar(0.5))
+        .pow(Scalar(2.5))
+        .match_on(["label"]);
+    utils::send_query(query).await.unwrap();
+}
+
+#[tokio::test]
+async fn arithmetic_ignoring() {
+    let query = Metric::new("test_metric")
+        .label_eq("label", "value")
+        .add(Metric::new("another_metric").label_ne("label", "value"))
+        .mul(Scalar(0.1))
+        .sub(Metric::new("third_metric"))
+        .div(Scalar(0.5))
+        .pow(Scalar(2.5))
+        .ignoring(["label"]);
+    utils::send_query(query).await.unwrap();
+}
+
+#[tokio::test]
+async fn comparison() {
+    let query = Metric::new("test_metric")
+        .label_eq("label", "value")
+        .cmp_eq(Metric::new("another_metric").label_ne("label", "value"))
+        .cmp_ne_bool(Scalar(0.1))
+        .cmp_gt(Metric::new("third_metric"));
+
+    utils::send_query(query).await.unwrap();
+}
+
+#[tokio::test]
+async fn comparison_match_on() {
+    let query = Metric::new("test_metric")
+        .label_eq("label", "value")
+        .cmp_eq(Metric::new("another_metric").label_ne("label", "value"))
+        .cmp_ne_bool(Scalar(0.1))
+        .cmp_gt(Metric::new("third_metric"))
+        .match_on(["label"]);
+
+    utils::send_query(query).await.unwrap();
+}
+
+#[tokio::test]
+async fn comparison_ignoring() {
+    let query = Metric::new("test_metric")
+        .label_eq("label", "value")
+        .cmp_eq(Metric::new("another_metric").label_ne("label", "value"))
+        .cmp_ne_bool(Scalar(0.1))
+        .cmp_gt(Metric::new("third_metric"))
+        .ignoring(["label"]);
+
+    utils::send_query(query).await.unwrap();
+}
+
+#[tokio::test]
+async fn logical_set() {
+    let query = Metric::new("test_metric")
+        .and(Metric::new("another_metric"))
+        .or(Metric::new("third_metric"))
+        .unless(Metric::new("fourth_metric"));
+    utils::send_query(query).await.unwrap();
+}
diff --git a/tests/query.rs b/tests/query.rs
new file mode 100644
index 0000000000000000000000000000000000000000..69c2002eb929f75b92f5bab55c0bfd656cf88c70
--- /dev/null
+++ b/tests/query.rs
@@ -0,0 +1,43 @@
+use mquery::query::{
+    ops::{Arithmetic, Comparison, Logical},
+    Label, Metric, Scalar,
+};
+
+mod utils;
+
+// Arbitrary queries to test whether they produce valid syntax
+// The server will return an error if there is a syntax error and the test will fail
+
+#[tokio::test]
+async fn basic_query_with_labels() {
+    let query = Metric::new("test_metric").labels([
+        Label::Eq("eq", "eqvalue"),
+        Label::RxEq("rxeq", ".*"),
+        Label::RxNe("rxne", ".*"),
+    ]);
+    utils::send_query(query).await.unwrap();
+}
+
+#[tokio::test]
+async fn arithmetic_ops() {
+    let query = Metric::new("test_metric")
+        .add(Metric::new("another_metric"))
+        .add(Scalar(0.5));
+    utils::send_query(query).await.unwrap();
+}
+
+#[tokio::test]
+async fn comp_ops() {
+    let query = Metric::new("test_metric")
+        .cmp_eq(Metric::new("another_metric"))
+        .cmp_ne_bool(Scalar(0.5));
+    utils::send_query(query).await.unwrap();
+}
+
+#[tokio::test]
+async fn logic_ops() {
+    let query = Metric::new("test_metric")
+        .and(Metric::new("another_metric"))
+        .and(Metric::new("third_metric"));
+    utils::send_query(query).await.unwrap();
+}
diff --git a/tests/utils.rs b/tests/utils.rs
new file mode 100644
index 0000000000000000000000000000000000000000..dbaa3a86db7e4f85e5c5b53bf574c6e2e51ef0e0
--- /dev/null
+++ b/tests/utils.rs
@@ -0,0 +1,9 @@
+use mquery::{query::IntoQuery, QueryManager};
+use prometheus_http_query::{response::PromqlResult, Error};
+
+// local victoria metrics server
+pub static URL: &str = "http://localhost:8428";
+
+pub async fn send_query(query: impl IntoQuery) -> Result<PromqlResult, Error> {
+    QueryManager::new(URL.parse().unwrap()).query(query).await
+}