Repository: shuttle-hq/synth (language: rust)

Issue:
Allow unbounded `number`s
**Required Functionality**
Currently `number` types can only be `range`, `categorical` (for integers) and `constant` (also some can be `Id`). There is no easy way to specify generating arbitrary numbers

**Proposed Solution**
Consider adding an `unbounded` variant that will generate any valid number of the given type, alternatively make serde use the range default if no argument at all is given.

**Use case**
Simplify the default, allow getting arbitrary numbers without much hassle


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index a86775ee..b6f6a4ab 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1887,11 +1887,11 @@ dependencies = [
 
 [[package]]
 name = "num"
-version = "0.3.1"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b7a8e9be5e039e2ff869df49155f1c06bd01ade2117ec783e56ab0932b67a8f"
+checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606"
 dependencies = [
- "num-bigint 0.3.3",
+ "num-bigint 0.4.2",
  "num-complex",
  "num-integer",
  "num-iter",
@@ -1919,6 +1919,7 @@ dependencies = [
  "autocfg 1.0.1",
  "num-integer",
  "num-traits",
+ "rand 0.8.4",
 ]
 
 [[package]]
@@ -1941,11 +1942,12 @@ dependencies = [
 
 [[package]]
 name = "num-complex"
-version = "0.3.1"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5"
+checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085"
 dependencies = [
  "num-traits",
+ "rand 0.8.4",
 ]
 
 [[package]]
@@ -1971,12 +1973,12 @@ dependencies = [
 
 [[package]]
 name = "num-rational"
-version = "0.3.2"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
+checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a"
 dependencies = [
  "autocfg 1.0.1",
- "num-bigint 0.3.3",
+ "num-bigint 0.4.2",
  "num-integer",
  "num-traits",
 ]
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 7bc97281..c28dad77 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -27,7 +27,7 @@ fake = { version = "2.4.1", features = ["http"] }
 rand = "0.8.3"
 chrono = { version = "0.4.18", features = ["serde"] }
 bincode = "1.3.1"
-num = "0.3.1"
+num = { version = "0.4.0", features = [ "rand" ] }
 rand_regex = "0.15.1"
 synth-gen = { path = "../gen", features = [ "shared" ] }
 uuid = { version = "0.8.2", features = ["v4"] }
diff --git a/core/src/graph/mod.rs b/core/src/graph/mod.rs
index 30897464..fbb7c42c 100644
--- a/core/src/graph/mod.rs
+++ b/core/src/graph/mod.rs
@@ -75,7 +75,7 @@ pub use string::{
 };
 
 pub mod number;
-pub use number::{Incrementing, NumberNode, RandomF64, RandomI64, RandomU64, UniformRangeStep};
+pub use number::{Incrementing, NumberNode, RandomF64, RandomI64, RandomU64, StandardIntRangeStep, StandardFloatRangeStep};
 
 pub mod boolean;
 pub use boolean::{BoolNode, RandomBool};
@@ -744,7 +744,11 @@ pub mod tests {
                             "generator": "safe_email"
                         }
                     },
-                    "num_logins_again": "@users.content.num_logins"
+                    "num_logins_again": "@users.content.num_logins",
+                    "a_large_number": {
+                        "type": "number",
+                        "range": {}
+                    }
                 }
             },
             "transactions": {
@@ -911,7 +915,7 @@ pub mod tests {
     #[test]
     fn range_distribution_u64() {
         let range = RangeStep::<u64>::new(15, 40, 5);
-        let dist = UniformRangeStep::try_from(range).unwrap();
+        let dist = StandardIntRangeStep::<u64, u64>::try_from_range(range).unwrap();
         let mut rng = thread_rng();
         for _ in 1..100 {
             match dist.sample(&mut rng) {
@@ -930,7 +934,7 @@ pub mod tests {
     #[test]
     fn range_distribution_i64() {
         let range = RangeStep::<i64>::new(-10, 10, 5);
-        let dist = UniformRangeStep::try_from(range).unwrap();
+        let dist = StandardIntRangeStep::<u64, i128>::try_from_range(range).unwrap();
         let mut rng = thread_rng();
         for _ in 1..100 {
             match dist.sample(&mut rng) {
@@ -948,7 +952,7 @@ pub mod tests {
     #[test]
     fn range_distribution_f64() {
         let range = RangeStep::new(-2.5, 1.0, 1.5);
-        let dist = UniformRangeStep::try_from(range).unwrap();
+        let dist = StandardFloatRangeStep::<f64>::try_from_range(range).unwrap();
         let mut rng = thread_rng();
         let error_margin = f64::EPSILON;
         for _ in 1..1000 {
@@ -968,13 +972,13 @@ pub mod tests {
     #[test]
     fn range_distribution_constant() {
         let range = RangeStep::<u64>::new(10, 10, 5);
-        assert!(UniformRangeStep::try_from(range).is_err())
+        assert!(StandardIntRangeStep::<u64, u64>::try_from_range(range).is_err())
     }
 
     #[test]
     fn range_distribution_step_larger_than_delta() {
         let range = RangeStep::<u64>::new(10, 15, 10);
-        let dist = UniformRangeStep::try_from(range).unwrap();
+        let dist = StandardIntRangeStep::<u64, u64>::try_from_range(range).unwrap();
         let mut rng = thread_rng();
         for _ in 1..100 {
             match dist.sample(&mut rng) {
@@ -989,7 +993,7 @@ pub mod tests {
     #[test]
     fn range_distribution_step_is_delta() {
         let range = RangeStep::<u64>::new(10, 15, 5);
-        let dist = UniformRangeStep::try_from(range).unwrap();
+        let dist = StandardIntRangeStep::<u64, u64>::try_from_range(range).unwrap();
         let mut rng = thread_rng();
         for _ in 1..100 {
             match dist.sample(&mut rng) {
diff --git a/core/src/graph/number.rs b/core/src/graph/number.rs
index 4acfa892..14ec9a27 100644
--- a/core/src/graph/number.rs
+++ b/core/src/graph/number.rs
@@ -1,51 +1,295 @@
 use super::prelude::*;
 
-use rand::distributions::uniform::SampleUniform;
+use rand::distributions::uniform::SampleRange;
 
-use num::{CheckedAdd, One, Zero};
+use num::{One, Zero, CheckedAdd};
 
-use std::ops::{Add, Rem, Sub};
+use std::ops::{Bound, RangeBounds};
 
-pub struct UniformRangeStep<N>(RangeStep<N>);
+/// A custom extension of all the range types that uses
+/// [`Bound`](std::ops::Bound) on the left and on the right.
+#[derive(Clone)]
+struct AnyRange<N> {
+    low: Bound<N>,
+    high: Bound<N>
+}
 
-impl<N: PartialOrd + Zero + Display> TryFrom<RangeStep<N>> for UniformRangeStep<N> {
-    type Error = anyhow::Error;
+macro_rules! any_range_int_impl {
+    { $target:ty } => {
+        impl AnyRange<$target> {
+            fn is_empty(&self) -> bool {
+                match (&self.low, &self.high) {
+                    (Bound::Excluded(low), Bound::Included(high))
+                    | (Bound::Included(low), Bound::Excluded(high)) => low >= high,
+                    (Bound::Included(low), Bound::Included(high)) => low > high,
+                    (Bound::Excluded(low), Bound::Excluded(high)) => *low + 1 >= *high,
+                    _ => false
+                }
+            }
+        }
+
+        impl SampleRange<$target> for AnyRange<$target> {
+            fn sample_single<R: rand::RngCore + ?Sized>(self, rng: &mut R) -> $target {
+                let low = match self.low {
+                    Bound::Unbounded => panic!("cannot sample {} range unbounded on the left", stringify!($target)),
+                    Bound::Included(low) => low,
+                    Bound::Excluded(low) => low + 1
+                };
+
+                match self.high {
+                    Bound::Excluded(high) => rng.gen_range(low..high),
+                    Bound::Included(high) => rng.gen_range(low..=high),
+                    Bound::Unbounded => panic!("cannot sample {} range unbounded on the right", stringify!($target))
+                }
+            }
 
-    fn try_from(range: RangeStep<N>) -> Result<Self, Self::Error> {
-        if range.low >= range.high {
-            return Err(
-                failed!(target: Debug, BadRequest => "cannot create a distribution for a Range where 'low' ({}) is greater than or equal to 'high' ({}).", range.low, range.high),
-            );
+            fn is_empty(&self) -> bool {
+                Self::is_empty(self)
+            }
         }
-        if range.step <= N::zero() {
-            return Err(
-                failed!(target: Debug, BadRequest => "cannot create a distribution for a Range where 'step' ({}) is less than or equal to 0.", range.step),
-            );
+    }
+}
+
+any_range_int_impl! { u32 }
+any_range_int_impl! { u64 }
+
+macro_rules! any_range_float_impl {
+    { $target:ty } => {
+        impl AnyRange<$target> {
+            fn is_not_empty(&self) -> bool {
+                match (&self.low, &self.high) {
+                    (Bound::Excluded(low), Bound::Included(high))
+                    | (Bound::Included(low), Bound::Excluded(high)) => low < high,
+                    (Bound::Included(low), Bound::Included(high)) => low <= high,
+                    _ => true
+                }
+            }
+        }
+
+        impl SampleRange<$target> for AnyRange<$target> {
+            fn sample_single<R: rand::RngCore + ?Sized>(self, rng: &mut R) -> $target {
+                let low = match self.low {
+                    Bound::Excluded(_)
+                    | Bound::Unbounded => panic!("cannot sample {} range unbounded or open on the left", stringify!($target)),
+                    Bound::Included(low) => low,
+                };
+
+                let (high, include_high) = match self.high {
+                    Bound::Unbounded => panic!("cannot sample {} range unbounded on the right", stringify!($target)),
+                    Bound::Included(high) => (high, true),
+                    Bound::Excluded(high) => (high, false)
+                };
+
+                if include_high {
+                    rng.gen_range(low..=high)
+                } else {
+                    rng.gen_range(low..high)
+                }
+            }
+
+            fn is_empty(&self) -> bool {
+                // This is a double-negative because of NaNs.
+                !Self::is_not_empty(self)
+            }
         }
-        Ok(Self(range))
     }
 }
 
-impl<N> Distribution<N> for UniformRangeStep<N>
-    where
-        N: Zero
-        + Add<Output=N>
-        + Sub<Output=N>
-        + Rem<Output=N>
-        + SampleUniform
-        + Copy
-        + PartialOrd,
+any_range_float_impl! { f32 }
+any_range_float_impl! { f64 }
+
+impl<N: std::fmt::Display> AnyRange<N> {
+    fn display_bound(bound: &Bound<N>, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match bound {
+            Bound::Included(n) => write!(f, "{} (inclusive)", n),
+            Bound::Excluded(n) => write!(f, "{} (exclusive)", n),
+            Bound::Unbounded => write!(f, "unbounded")
+        }
+    }
+}
+
+impl<N> std::fmt::Display for AnyRange<N>
+where
+    N: std::fmt::Display
+{
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "low=")?;
+        Self::display_bound(&self.low, f)?;
+        write!(f, " high=")?;
+        Self::display_bound(&self.high, f)
+    }
+}
+
+impl<N: Clone> AnyRange<N> {
+    fn from_range_bounds<R: RangeBounds<N>>(r: &R) -> Self {
+        Self {
+            low: r.start_bound().cloned(),
+            high: r.end_bound().cloned()
+        }
+    }
+}
+
+pub struct StandardIntRangeStep<R, L> {
+    range: AnyRange<R>,
+    step: R,
+    low: L
+}
+
+impl<R, L> std::fmt::Display for StandardIntRangeStep<R, L>
+where
+    R: std::fmt::Display,
+    L: std::fmt::Display
 {
-    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> N {
-        let low = self.0.low;
-        let high = self.0.high;
-        let step = self.0.step;
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.range.fmt(f)?;
+        write!(f, " step={}", self.step)
+    }
+}
+
+fn into_finite_bound<N>(n: N, inclusive: bool) -> Bound<N> {
+    if inclusive {
+        Bound::Included(n)
+    } else {
+        Bound::Excluded(n)
+    }
+}
+
+/// Macroed implementation of [`Distribution`](rand::distributions::Distribution) for integer
+/// primitive ranges.
+///
+/// The default behavior is to cast the lower and upper bounds of the range (instances
+/// of `$target`) to `$larger`, where `high - low` is computed and truncated down to `$unsigned`.
+///
+/// In particular when `$target == $larger == $unsigned` is an unsigned integer type these are
+/// no-ops.
+macro_rules! standard_int_range_step_impl {
+    { $target:ty, $larger:ty, $unsigned:ty } => {
+        impl StandardIntRangeStep<$unsigned, $larger> {
+            pub fn try_from_range(range_step: RangeStep<$target>) -> anyhow::Result<Self> {
+                let low = range_step.low.unwrap_or(<$target>::MIN);
+                let high = range_step.high.unwrap_or(<$target>::MAX);
+                if low > high {
+                    return Err(anyhow!("integer range cannot have 'low'={} > 'high'={}", low, high))
+                }
+
+                let delta = ((high as $larger) - (low as $larger)) as $unsigned;
+
+                let step_unchecked = range_step.step.unwrap_or(1);
+
+                #[allow(unused_comparisons)]
+                if step_unchecked == 0 {
+                    return Err(anyhow!("integer range 'step'=0 is invalid, use the constant generator instead"))
+                } else if step_unchecked < 0 {
+                    return Err(anyhow!("integer range 'step'={} is invalid, use a positive value instead", step_unchecked))
+                }
+
+                let step = step_unchecked as $unsigned;
+
+                let include_low = range_step.include_low;
+                let include_high = range_step.include_high;
 
-        let temp: N = rng.gen_range(N::zero()..(high - low));
-        low + temp - (temp % step)
+                let delta_include_high = if delta % step == 0 {
+                    include_high
+                } else {
+                    true
+                };
+
+                let delta_high = into_finite_bound(delta / step, delta_include_high);
+                let delta_low = into_finite_bound(0, include_low);
+                let range = AnyRange::from_range_bounds(&(delta_low, delta_high));
+
+                if range.is_empty() {
+                    let original = AnyRange::from_range_bounds(&range_step);
+                    Err(anyhow!("{} range with {} step={} is empty", stringify!($target), original, step))
+                } else {
+                    Ok(Self { range, low: low as $larger, step })
+                }
+            }
+        }
+
+        impl Distribution<$target> for StandardIntRangeStep<$unsigned, $larger> {
+            fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> $target {
+                let num_steps = rng.gen_range(self.range.clone());
+                let delta = num_steps * self.step;
+                (delta as $larger + self.low) as $target
+            }
+        }
+    }
+}
+
+standard_int_range_step_impl! { i32, i64, u32 }
+standard_int_range_step_impl! { u32, u32, u32 }
+standard_int_range_step_impl! { i64, i128, u64 }
+standard_int_range_step_impl! { u64, u64, u64 }
+
+pub struct StandardFloatRangeStep<N> {
+    range: AnyRange<N>,
+    low: Option<N>,
+    step: Option<N>
+}
+
+macro_rules! standard_float_range_step_impl {
+    { $target:ty } => {
+        impl StandardFloatRangeStep<$target> {
+            pub fn try_from_range(range_step: RangeStep<$target>) -> anyhow::Result<Self> {
+                let step = range_step.step;
+                if Some(true) == step.as_ref().map(|step| *step <= 0.) {
+                    return Err(anyhow!("{} range with step={} is invalid, use positive values instead", stringify!($target), step.unwrap()));
+                }
+
+                let mut low = range_step.low.unwrap_or(0.);
+                if !range_step.include_low {
+                    let step = step.as_ref()
+                        .ok_or_else(|| anyhow!("{} range with include_low=false must set the 'step' parameter", stringify!($target)))?;
+                    low += *step;
+                }
+                let low_bound = into_finite_bound(low, true);
+
+                let high = range_step.high.unwrap_or(1.);
+                let high_bound = into_finite_bound(high, range_step.include_high);
+
+                // Check this here because [`Standard`](rand::distributions::Standard) will panic
+                // otherwise.
+                if !low.is_finite() || !high.is_finite() {
+                    return Err(anyhow!("{} range with low={} high={} is invalid", stringify!($target), low, high));
+                }
+
+                // Check this here because [`Standard`](rand::distributions::Standard) will panic
+                // otherwise.
+                if !(high - low).is_finite() {
+                    return Err(anyhow!("{} range with low={} high={} has overflowed: try smaller bounds", stringify!($target), low, high))
+                }
+
+                let range = AnyRange::from_range_bounds(&(low_bound, high_bound));
+
+                if range.is_empty() {
+                    let step = step.map(|s| format!("step={}", s)).unwrap_or_default();
+                    Err(anyhow!("{} range with {} {} is empty", stringify!($target), range, step))
+                } else {
+                    let low = if step.is_some() { Some(low) } else { None };
+                    Ok(Self { range, low, step })
+                }
+            }
+        }
+
+        impl Distribution<$target> for StandardFloatRangeStep<$target> {
+            fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> $target {
+                let num = rng.gen_range(self.range.clone());
+                if let Some(step) = self.step.as_ref() {
+                    // If `step` is defined, attempt to align to it
+                    let low = self.low.as_ref().unwrap();
+                    (num - low).div_euclid(*step) * *step + low
+                } else {
+                    num
+                }
+            }
+        }
     }
 }
 
+standard_float_range_step_impl! { f32 }
+standard_float_range_step_impl! { f64 }
+
 pub struct Incrementing<N = i64> {
     count: N,
     step: N,
@@ -119,7 +363,7 @@ impl<N> Generator for Incrementing<N>
                 } else {
                     GeneratorState::Complete(Err(failed_crate!(
                         target: Release,
-                        "incrementing generator overflowed: try using a larger type"
+                        "incrementing generator overflowed: try specifying the \'subtype\' parameter with a larger numerical primitive (e.g. u64)"
                     )))
                 }
             }
@@ -128,10 +372,9 @@ impl<N> Generator for Incrementing<N>
 }
 
 macro_rules! number_node {
-    {
-	$(
+    { $(
 	    $rand:ident (
-		$range:ident as $new_range:ident,
+		$range:ident<$dist:ty> as $new_range:ident,
 		$constant:ident as $new_constant:ident,
 		$(
 		    $categorical:ident as $new_categorical:ident
@@ -140,98 +383,91 @@ macro_rules! number_node {
 		    $incrementing:ident as $new_incrementing:ident
 		)?,
 	    ) for $ty:ty,
-	)*
-    } => {
-	$(
-	    derive_generator! {
-		yield $ty,
-		return Result<$ty, Error>,
-		pub enum $rand {
-		    $range(OnceInfallible<Random<$ty, UniformRangeStep<$ty>>>),
-		    $constant(OnceInfallible<Yield<$ty>>),
-		    $($categorical(OnceInfallible<Random<$ty, Categorical<$ty>>>),)?
-		    $($incrementing(TryOnce<Incrementing<$ty>>),)?
-		}
-	    }
-	)*
-
-	$(
-	    impl $rand {
-		pub fn $new_range(range: RangeStep<$ty>) -> Result<Self, anyhow::Error> {
-		    let dist = UniformRangeStep::try_from(range)?;
-		    Ok(Self::$range(Random::new_with(dist).infallible().try_once()))
-		}
-
-		pub fn $new_constant(value: $ty) -> Self {
-		    Self::$constant(Yield::wrap(value).infallible().try_once())
-		}
+	)* } => {
+        derive_generator! {
+            yield Token,
+            return Result<Value, Error>,
+            pub enum NumberNode {
+                $($rand(Valuize<Tokenizer<$rand>, $ty>),)*
+            }
+        }
 
-		$(
-		    pub fn $new_categorical(cat: Categorical<$ty>) -> Self {
-			Self::$categorical(Random::new_with(cat).infallible().try_once())
-		    }
-		)?
+        $(
+        derive_generator! {
+            yield $ty,
+            return Result<$ty, Error>,
+            pub enum $rand {
+                $range(OnceInfallible<Random<$ty, $dist>>),
+                $constant(OnceInfallible<Yield<$ty>>),
+                $($categorical(OnceInfallible<Random<$ty, Categorical<$ty>>>),)?
+                $($incrementing(TryOnce<Incrementing<$ty>>),)?
+            }
+        }
 
-		$(
-		    pub fn $new_incrementing(incr: Incrementing<$ty>) -> Self {
-			Self::$incrementing(incr.try_once())
-		    }
-		)?
-	    }
-	)*
-
-	derive_generator! {
-	    yield Token,
-	    return Result<Value, Error>,
-	    pub enum NumberNode {
-		$(
-		    $rand(Valuize<Tokenizer<$rand>, $ty>),
-		)*
-	    }
-	}
-
-	$(
-	    impl From<$rand> for NumberNode {
-		fn from(value: $rand) -> Self {
-		    Self::$rand(value.into_token().map_complete(value_from_ok_number::<$ty>))
-		}
-	    }
-	)*
+        impl $rand {
+            pub fn $new_range(range: RangeStep<$ty>) -> Result<Self, anyhow::Error> {
+                let dist = <$dist>::try_from_range(range)?;
+                Ok(Self::$range(Random::new_with(dist).infallible().try_once()))
+            }
+
+            pub fn $new_constant(value: $ty) -> Self {
+                Self::$constant(Yield::wrap(value).infallible().try_once())
+            }
+
+            $(
+            pub fn $new_categorical(cat: Categorical<$ty>) -> Self {
+                Self::$categorical(Random::new_with(cat).infallible().try_once())
+            }
+            )?
+
+            $(
+            pub fn $new_incrementing(incr: Incrementing<$ty>) -> Self {
+                Self::$incrementing(incr.try_once())
+            }
+            )?
+        }
+
+        impl From<$rand> for NumberNode {
+            fn from(value: $rand) -> Self {
+                Self::$rand(value.into_token().map_complete(value_from_ok_number::<$ty>))
+            }
+        }
+        )*
     }
 }
 
 number_node!(
     RandomU64 (
-    U64Range as range,
-    U64Constant as constant,
-    U64Categorical as categorical,
-    Incrementing as incrementing,
+        U64Range<StandardIntRangeStep<u64, u64>> as range,
+        U64Constant as constant,
+        U64Categorical as categorical,
+        Incrementing as incrementing,
     ) for u64,
     RandomI64 (
-    I64Range as range,
-    I64Constant as constant,
-    I64Categorical as categorical,
-    Incrementing as incrementing,
+        I64Range<StandardIntRangeStep<u64, i128>> as range,
+        I64Constant as constant,
+        I64Categorical as categorical,
+        Incrementing as incrementing,
     ) for i64,
     RandomF64 (
-    F64Range as range,
-    F64Constant as constant,,,
+        F64Range<StandardFloatRangeStep<f64>> as range,
+        F64Constant as constant,,,
     ) for f64,
     RandomU32 (
-    U32Range as range,
-    U32Constant as constant,
-    U32Categorical as categorical,
-    Incrementing as incrementing,
+        U32Range<StandardIntRangeStep<u32, u32>> as range,
+        U32Constant as constant,
+        U32Categorical as categorical,
+        Incrementing as incrementing,
     ) for u32,
     RandomI32 (
-    I32Range as range,
-    I32Constant as constant,
-    I32Categorical as categorical,
-    Incrementing as incrementing,
+        I32Range<StandardIntRangeStep<u32, i64>> as range,
+        I32Constant as constant,
+        I32Categorical as categorical,
+        Incrementing as incrementing,
     ) for i32,
     RandomF32 (
-    F32Range as range,
-    F32Constant as constant,,,
+        F32Range<StandardFloatRangeStep<f32>> as range,
+        F32Constant as constant,,,
     ) for f32,
 );
 
diff --git a/core/src/graph/unique.rs b/core/src/graph/unique.rs
index d2eeb4db..0e13029a 100644
--- a/core/src/graph/unique.rs
+++ b/core/src/graph/unique.rs
@@ -74,12 +74,7 @@ pub mod tests {
         assert_eq!(output.len(), NUM_GENERATED);
 
         let numbers = Graph::Number(NumberNode::from(
-            RandomU64::range(RangeStep {
-                low: 0,
-                high: NUM_GENERATED as u64,
-                step: 1,
-            })
-            .unwrap(),
+            RandomU64::range(RangeStep::new(0, NUM_GENERATED as u64, 1)).unwrap(),
         ));
         let output = UniqueNode::hash(numbers, None)
             .repeat(NUM_GENERATED)
diff --git a/core/src/schema/content/array.rs b/core/src/schema/content/array.rs
index 0ae56251..85417308 100644
--- a/core/src/schema/content/array.rs
+++ b/core/src/schema/content/array.rs
@@ -13,11 +13,7 @@ pub struct ArrayContent {
 impl ArrayContent {
     pub fn from_content_default_length(content: Content) -> Self {
         Self {
-            length: Box::new(Content::Number(NumberContent::U64(U64::Range(RangeStep {
-                low: 1,
-                high: 2,
-                step: 1,
-            })))),
+            length: Box::new(Content::Number(NumberContent::U64(U64::Range(RangeStep::new(1, 2, 1))))),
             content: Box::new(content),
         }
     }
diff --git a/core/src/schema/content/mod.rs b/core/src/schema/content/mod.rs
index dca860c9..10bc5ec9 100644
--- a/core/src/schema/content/mod.rs
+++ b/core/src/schema/content/mod.rs
@@ -412,25 +412,13 @@ impl<'r> From<&'r Value> for Content {
             Value::Number(number_value) => {
                 let number_content = if number_value.is_f64() {
                     let value = number_value.as_f64().unwrap();
-                    NumberContent::F64(number_content::F64::Range(RangeStep {
-                        low: value,
-                        high: value + 1.0,
-                        step: 1.,
-                    }))
+                    NumberContent::F64(number_content::F64::Range(RangeStep::new(value, value + 1., 1.)))
                 } else if number_value.is_u64() {
                     let value = number_value.as_u64().unwrap();
-                    NumberContent::U64(number_content::U64::Range(RangeStep {
-                        low: value,
-                        high: value + 1,
-                        step: 1,
-                    }))
+                    NumberContent::U64(number_content::U64::Range(RangeStep::new(value, value + 1, 1)))
                 } else if number_value.is_i64() {
                     let value = number_value.as_i64().unwrap();
-                    NumberContent::I64(number_content::I64::Range(RangeStep {
-                        low: value,
-                        high: value + 1,
-                        step: 1,
-                    }))
+                    NumberContent::I64(number_content::I64::Range(RangeStep::new(value, value + 1, 1)))
                 } else {
                     unreachable!()
                 };
diff --git a/core/src/schema/content/number.rs b/core/src/schema/content/number.rs
index 8dc6d59a..d64ed12a 100644
--- a/core/src/schema/content/number.rs
+++ b/core/src/schema/content/number.rs
@@ -1,7 +1,7 @@
 use super::prelude::*;
 
 use super::Categorical;
-use num::Zero;
+
 use serde::{
     de::{Deserialize, Deserializer},
     ser::Serializer,
@@ -66,224 +66,264 @@ macro_rules! number_content {
 	    $ty:ty[$is:ident, $def:ident] as $as:ident {
 		$(
 		    $(#[$default:meta])?
-		    $variant:ident($variant_ty:ty),
+		    $variant:ident($variant_ty:path),
 		)*
 	    },
 	)*
     } => {
-	#[derive(Debug, Clone, PartialEq)]
-    //TODO: With custom implementation for `Serialize` we
-    //      have to implement this on our own:
-	// #[serde(deny_unknown_fields)]
-	pub enum NumberContent {
-	    $(
-		$as(number_content::$as),
-	    )*
-	}
-
-    impl Serialize for NumberContent
-    {
-        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-        where
-            S: Serializer,
-        {
-            match &self {
-                $(
-                    NumberContent::$as(value) => {
-                        let mut obj = serde_json::to_value(value).map_err(S::Error::custom)?;
-                        let obj_map = obj.as_object_mut().ok_or(S::Error::custom("Object value expected"))?;
-                        obj_map.insert("subtype".to_string(), serde_convert_case!($as, "snake_case"));
-                        obj_map.serialize(serializer)
-                    }
-                )*
+        #[derive(Debug, Clone, PartialEq)]
+        pub enum NumberContent {
+            $($as(number_content::$as),)*
+        }
+
+        impl NumberContent {
+            pub fn kind(&self) -> &'static str {
+                match self {
+                    $($(Self::$as(number_content::$as::$variant(_)) => {
+                        concat!(stringify!($as), "::", stringify!($variant))
+                    },)*)*
+                }
+            }
+
+            $(
+            pub fn $def() -> Self {
+                Self::$as(number_content::$as::Range(RangeStep::default()))
+            }
+
+            pub fn $is(&self) -> bool {
+                match self {
+                Self::$as(_) => true,
+                _ => false
+                }
             }
+            )*
         }
-    }
 
-    impl<'de> Deserialize<'de> for NumberContent {
-        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-        where
-            D: Deserializer<'de>,
+        impl Serialize for NumberContent
         {
-            let mut v: serde_json::Value = Value::deserialize(deserializer)?;
-            v.as_object_mut().ok_or(D::Error::custom("Object value expected"))?.remove("type");
-            match Option::<serde_json::Value>::deserialize(&v["subtype"]).map_err(D::Error::custom)? {
-                Some(subtype) => {
-                $(
-                    if subtype == serde_convert_case!($as, "snake_case") {
-                        v.as_object_mut().ok_or(D::Error::custom("Object value expected"))?
-                                         .remove("subtype");
-                        let inner = number_content::$as::deserialize(v).map_err(D::Error::custom)?;
-                        Ok(NumberContent::$as(inner))
-                    }
-                    else
-                )*
-                    {
-                        //TODO: generate static array with variant names and pass it to
-                        //      Error::unknown_variant()
-                        Err(D::Error::unknown_variant(format!("{:?}", subtype).as_str(), &[""]))
-                    }
+            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+            where
+                S: Serializer,
+            {
+                match &self {
+                    $(
+                        NumberContent::$as(value) => {
+                            let mut obj = serde_json::to_value(value).map_err(S::Error::custom)?;
+                            let obj_map = obj.as_object_mut().ok_or(S::Error::custom("Object value expected"))?;
+                            obj_map.insert("subtype".to_string(), serde_convert_case!($as, "snake_case"));
+                            obj_map.serialize(serializer)
+                        }
+                    )*
                 }
-                None => {
-                    if let Ok(inner) = number_content::U64::deserialize(&v) {
-                        Ok(NumberContent::U64(inner))
-                    } else
-                    if let Ok(inner) = number_content::I64::deserialize(&v) {
-                        Ok(NumberContent::I64(inner))
-                    } else
-                    if let Ok(inner) = number_content::F64::deserialize(&v) {
-                        Ok(NumberContent::F64(inner))
-                    } else {
-                        Err(D::Error::custom("Failed to infer numeric type from it's value"))
+            }
+        }
+
+        impl<'de> Deserialize<'de> for NumberContent {
+            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+            where
+                D: Deserializer<'de>,
+            {
+                let mut v: serde_json::Value = Value::deserialize(deserializer)?;
+                let as_object = v.as_object_mut().ok_or(D::Error::custom("Object value expected"))?;
+                match as_object.remove("subtype") {
+                    Some(subtype) => {
+                    $(
+                        if subtype == stringify!($ty) {
+                            if as_object.is_empty() {
+                                Ok(Self::$def())
+                            } else {
+                                let inner = number_content::$as::deserialize(v).map_err(D::Error::custom)?;
+                                Ok(NumberContent::$as(inner))
+                            }
+                        }
+                        else
+                    )*
+                        {
+                            Err(D::Error::unknown_variant(format!("{:?}", subtype).as_str(), &[""]))
+                        }
+                    }
+                    None => {
+                        $( if let Ok(inner) = number_content::$as::deserialize(&v) {
+                            Ok(NumberContent::$as(inner))
+                        } else )* {
+                            Err(D::Error::custom("Failed to infer numeric type from its value: try specifying the 'subtype' parameter"))
+                        }
                     }
                 }
             }
         }
-    }
 
-	pub mod number_content {
-	    use super::{RangeStep, Categorical, NumberContent};
-	    use serde::{Serialize, Deserialize};
-
-	    $(
-		#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
-		#[serde(rename_all = "snake_case")]
-		#[serde(deny_unknown_fields)]
-		pub enum $as {
-		    $(
-			$variant($variant_ty),
-		    )*
-		}
-
-        $(
-        impl From<$variant_ty> for $as {
-            fn from(value: $variant_ty) -> Self {
-                Self::$variant(value)
+        pub mod number_content {
+            use super::{RangeStep, Categorical, NumberContent, Id};
+            use serde::{Serialize, Deserialize};
+
+            $(
+            #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
+            #[serde(rename_all = "snake_case")]
+            #[serde(deny_unknown_fields)]
+            pub enum $as {
+                $($variant($variant_ty),)*
             }
-        }
-        )*
-
-		impl From<$as> for NumberContent {
-		    fn from(value: $as) -> Self {
-			Self::$as(value)
-		    }
-		}
-	    )*
-	}
-
-	#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
-	#[serde(deny_unknown_fields)]
-    pub struct Id {
-        #[serde(default)]
-        #[serde(skip_serializing_if = "Option::is_none")]
-        pub start_at: Option<u64>,
-    }
 
-	impl NumberContent {
-	    pub fn accepts(&self, number: &Number) -> Result<()> {
-		if self.is_u64() && number.is_u64()
-                    || self.is_i64() && number.is_i64()
-                    || self.is_f64() && number.is_f64()
-                {
-                    Ok(())
-                } else {
-		    // TODO: better error
-                    Err(failed!(target: Release, "numerical type mismatch"))
+            $(
+            impl From<$variant_ty> for $as {
+                fn from(value: $variant_ty) -> Self {
+                    Self::$variant(value)
                 }
-	    }
+            }
+            )*
 
-	    pub fn kind(&self) -> &'static str {
-		match self {
-		    $(
-			$(
-			    Self::$as(number_content::$as::$variant(_)) => {
-				concat!(stringify!($as), "::", stringify!($variant))
-			    },
-			)*
-		    )*
-		}
-	    }
+            impl From<$as> for NumberContent {
+                fn from(value: $as) -> Self {
+                    Self::$as(value)
+                }
+            }
+            )*
+        }
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
+#[serde(deny_unknown_fields)]
+pub struct Id {
+    #[serde(default)]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub start_at: Option<u64>,
+}
 
-	    $(
-		pub fn $def() -> Self {
-		    Self::$as(number_content::$as::Range(RangeStep::default()))
-		}
-
-		pub fn $is(&self) -> bool {
-		    match self {
-			Self::$as(_) => true,
-			_ => false
-		    }
-		}
-	    )*
-	}
+impl NumberContent {
+    pub fn accepts(&self, number: &Number) -> Result<()> {
+        if self.is_u64() && number.is_u64()
+            || self.is_i64() && number.is_i64()
+            || self.is_f64() && number.is_f64()
+        {
+            Ok(())
+        } else {
+            // TODO: better error
+            Err(failed!(target: Release, "numerical type mismatch"))
+        }
     }
 }
 
 #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
 #[serde(deny_unknown_fields)]
+#[serde(default)]
 pub struct RangeStep<N> {
-    pub low: N,
-    pub high: N,
-    pub step: N,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub low: Option<N>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub high: Option<N>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub step: Option<N>,
+    #[serde(skip_serializing_if = "std::clone::Clone::clone")]
+    pub include_low: bool,
+    #[serde(skip_serializing_if = "std::ops::Not::not")]
+    pub include_high: bool
 }
 
-impl<N: PartialOrd + Zero + Display> RangeStep<N> {
-    #[allow(dead_code)]
-    pub(crate) fn new(low: N, high: N, step: N) -> Self {
-        Self { low, high, step }
+impl<N> Default for RangeStep<N> {
+    fn default() -> Self {
+        Self {
+            low: None,
+            high: None,
+            step: None,
+            include_low: true,
+            include_high: false
+        }
     }
 }
 
-impl<N: PartialOrd + Zero + Display> Default for RangeStep<N>
-where
-    N: Bounded + One,
-{
-    fn default() -> Self {
+impl<N> RangeStep<N> {
+    pub fn new(low: N, high: N, step: N) -> Self {
         Self {
-            low: N::min_value(),
-            high: N::max_value(),
-            step: N::one(),
+            low: Some(low),
+            high: Some(high),
+            step: Some(step),
+            ..Default::default()
+        }
+    }
+
+    fn bound(value: Option<&N>, inclusive: bool) -> std::ops::Bound<&N> {
+        match value {
+            Some(n) if inclusive => std::ops::Bound::Included(n),
+            Some(n) => std::ops::Bound::Excluded(n),
+            None => std::ops::Bound::Unbounded
         }
     }
+
+    fn cast<F, M>(self,  f: F) -> RangeStep<M>
+        where
+            F: Fn(N) -> M
+    {
+        self.try_cast::<_, _, std::convert::Infallible>(|value| Ok(f(value))).unwrap()
+    }
+
+    fn try_cast<F, M, E>(self, f: F) -> Result<RangeStep<M>, E>
+        where
+            F: Fn(N) -> Result<M, E>
+    {
+        Ok(RangeStep::<M> {
+            low: self.low.map(&f).transpose()?,
+            high: self.high.map(&f).transpose()?,
+            step: self.step.map(&f).transpose()?,
+            include_low: self.include_low,
+            include_high: self.include_high
+        })
+    }
+}
+
+impl<N: Copy> RangeStep<N> {
+    pub fn step(&self) -> Option<N> {
+        self.step.as_ref().cloned()
+    }
+}
+
+impl<N> std::ops::RangeBounds<N> for RangeStep<N> {
+    fn start_bound(&self) -> std::ops::Bound<&N> {
+        RangeStep::bound(self.low.as_ref(), self.include_low)
+    }
+
+    fn end_bound(&self) -> std::ops::Bound<&N> {
+        RangeStep::bound(self.high.as_ref(), self.include_high)
+    }
 }
 
 number_content!(
+    u32[is_u32, default_u32_range] as U32 {
+        #[default]
+        Range(RangeStep<u32>),
+        Categorical(Categorical<u32>),
+        Constant(u32),
+        Id(Id),
+    },
     u64[is_u64, default_u64_range] as U64 {
-    #[default]
-    Range(RangeStep<u64>),
-    Categorical(Categorical<u64>),
-    Constant(u64),
-    Id(crate::schema::Id),
+        #[default]
+        Range(RangeStep<u64>),
+        Categorical(Categorical<u64>),
+        Constant(u64),
+        Id(Id),
+    },
+    i32[is_i32, default_i32_range] as I32 {
+        #[default]
+        Range(RangeStep<i32>),
+        Categorical(Categorical<i32>),
+        Constant(i32),
     },
     i64[is_i64, default_i64_range] as I64 {
-    #[default]
-    Range(RangeStep<i64>),
-    Categorical(Categorical<i64>),
-    Constant(i64),
+        #[default]
+        Range(RangeStep<i64>),
+        Categorical(Categorical<i64>),
+        Constant(i64),
     },
     f64[is_f64, default_f64_range] as F64 {
-    #[default]
-    Range(RangeStep<f64>),
-    Constant(f64),
-    },
-    u32[is_u32, default_u32_range] as U32 {
-    #[default]
-    Range(RangeStep<u32>),
-    Categorical(Categorical<u32>),
-    Constant(u32),
-    Id(crate::schema::Id),
-    },
-    i32[is_i32, default_i32_range] as I32 {
-    #[default]
-    Range(RangeStep<i32>),
-    Categorical(Categorical<i32>),
-    Constant(i32),
+        #[default]
+        Range(RangeStep<f64>),
+        Constant(f64),
     },
     f32[is_f32, default_f32_range] as F32 {
-    #[default]
-    Range(RangeStep<f32>),
-    Constant(f32),
+        #[default]
+        Range(RangeStep<f32>),
+        Constant(f32),
     },
 );
 
@@ -363,19 +403,11 @@ impl RangeStep<u64> {
         match to {
             NumberContentKind::U64 => Ok(number_content::U64::Range(self).into()),
             NumberContentKind::I64 => {
-                let cast = RangeStep {
-                    low: i64::try_from(self.low)?,
-                    high: i64::try_from(self.high)?,
-                    step: i64::try_from(self.step)?,
-                };
+                let cast = self.try_cast(i64::try_from)?;
                 Ok(number_content::I64::Range(cast).into())
             }
             NumberContentKind::F64 => {
-                let cast = RangeStep {
-                    low: self.low as f64,
-                    high: self.high as f64,
-                    step: self.step as f64,
-                };
+                let cast = self.cast(|value| value as f64);
                 Ok(number_content::F64::Range(cast).into())
             }
         }
@@ -416,11 +448,7 @@ impl RangeStep<i64> {
             )),
             NumberContentKind::I64 => Ok(number_content::I64::Range(self).into()),
             NumberContentKind::F64 => {
-                let cast = RangeStep {
-                    low: self.low as f64,
-                    high: self.high as f64,
-                    step: self.step as f64,
-                };
+                let cast = self.cast(|value| value as f64);
                 Ok(number_content::F64::Range(cast).into())
             }
         }
@@ -518,79 +546,49 @@ impl number_content::F64 {
 pub mod tests {
     use super::*;
 
-    #[test]
-    fn test_number_content_no_subtype() {
-        // I64
-        let number_content_as_json = json!(
-            {
-                "range": {
-                    "low": -10,
-                    "high": 4,
-                    "step": 1
-                }
-            }
-        );
-        let number_content: NumberContent = serde_json::from_value(number_content_as_json).unwrap();
-        assert_eq!(
-            number_content,
-            NumberContent::I64(number_content::I64::Range(RangeStep {
-                low: -10,
-                high: 4,
-                step: 1
-            }))
-        );
-        // U64
-        let number_content_as_json = json!(
-            {
-                "range": {
-                    "low": 1,
-                    "high": 4,
-                    "step": 1
-                }
-            }
-        );
-        let number_content: NumberContent = serde_json::from_value(number_content_as_json).unwrap();
-        assert_eq!(
-            number_content,
-            NumberContent::U64(number_content::U64::Range(RangeStep {
-                low: 1,
-                high: 4,
-                step: 1
-            }))
-        );
-        // F64
-        let number_content_as_json = json!(
-            {
-                "range": {
-                    "low": 274.4,
-                    "high": 6597.5,
-                    "step": 0.1
-                }
+    use num::One;
+
+    // TODO: f32 deserializes successfully to `inf` when OOR
+    macro_rules! test_number_variants {
+        ($($test:ident -> $name:literal $as:ident: $ty:ty $(,)?)+) => { $(
+            #[test]
+            fn $test() {
+                // Inferred
+                let number_content_as_json = json!({
+                    "range": {
+                        "low": <$ty>::MIN,
+                        "high": <$ty>::MAX
+                    }
+                });
+                let number_content: NumberContent = serde_json::from_value(number_content_as_json).unwrap();
+                assert_eq!(
+                    number_content,
+                    NumberContent::$as(number_content::$as::Range(RangeStep {
+                        low: Some(<$ty>::MIN),
+                        high: Some(<$ty>::MAX),
+                        step: None,
+                        ..Default::default()
+                    }))
+                );
+
+                // Specified
+                let number_content_as_json = json!({
+                    "subtype": $name,
+                    "constant": <$ty>::one()
+                });
+                let number_content: NumberContent = serde_json::from_value(number_content_as_json).unwrap();
+                assert_eq!(
+                    number_content,
+                    NumberContent::$as(number_content::$as::Constant(<$ty>::one()))
+                );
             }
-        );
-        let number_content: NumberContent = serde_json::from_value(number_content_as_json).unwrap();
-        assert_eq!(
-            number_content,
-            NumberContent::F64(number_content::F64::Range(RangeStep {
-                low: 274.4,
-                high: 6597.5,
-                step: 0.1
-            }))
-        );
+        )+ }
     }
 
-    #[test]
-    fn test_number_content_subtype() {
-        let number_content_as_json = json!(
-            {
-                "subtype": "i64",
-                "constant": -10
-            }
-        );
-        let number_content: NumberContent = serde_json::from_value(number_content_as_json).unwrap();
-        assert_eq!(
-            number_content,
-            NumberContent::I64(number_content::I64::Constant(-10))
-        );
-    }
+    test_number_variants!(
+        test_u32 -> "u32" U32: u32,
+        test_u64 -> "u64" U64: u64,
+        test_i32 -> "i32" I32: i32,
+        test_i64 -> "i64" I64: i64,
+    );
 }
diff --git a/core/src/schema/content/prelude.rs b/core/src/schema/content/prelude.rs
index e22de04c..481813b8 100644
--- a/core/src/schema/content/prelude.rs
+++ b/core/src/schema/content/prelude.rs
@@ -21,7 +21,6 @@ pub(super) use std::time::Duration as StdDuration;
 pub(super) use chrono::{
     DateTime, Duration, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc,
 };
-pub(super) use num::{Bounded, One};
 pub(super) use rand::{
     distributions::{Bernoulli, Distribution},
     Rng,
diff --git a/core/src/schema/inference/mod.rs b/core/src/schema/inference/mod.rs
index 1a77da59..9ceff770 100644
--- a/core/src/schema/inference/mod.rs
+++ b/core/src/schema/inference/mod.rs
@@ -12,7 +12,7 @@ pub use value::ValueMergeStrategy;
 use super::{
     number_content, ArrayContent, BoolContent, Categorical, ChronoValueFormatter, Content,
     DateTimeContent, Id, NumberContent, NumberKindExt, ObjectContent, OneOfContent, RangeStep,
-    StringContent, ValueKindExt,
+    StringContent, ValueKindExt, CategoricalType
 };
 
 pub trait MergeStrategy<M, C>: std::fmt::Display {
@@ -192,36 +192,32 @@ impl MergeStrategy<DateTimeContent, String> for OptionalMergeStrategy {
     }
 }
 
-macro_rules! merge_strategy_for_numbers {
-    (
-	RangeStep for $($num:ty,)+
-    ) => {$(
-	impl MergeStrategy<RangeStep<$num>, $num> for OptionalMergeStrategy {
-	    fn try_merge(self, master: &mut RangeStep<$num>, value: &$num) -> Result<()> {
-		master.low = (*value).min(master.low);
-		master.high = (*value).max(master.high);
-		Ok(())
-	    }
-	}
-    )+};
-    (
-	Categorical for $($num:ty,)*
-    ) => {$(
-	impl MergeStrategy<Categorical<$num>, $num> for OptionalMergeStrategy {
-	    fn try_merge(
-		self,
-		master: &mut Categorical<$num>,
-		value: &$num
-	    ) -> Result<()> {
-		master.push(*value);
-		Ok(())
-	    }
-	}
-    )*}
+impl<N> MergeStrategy<RangeStep<N>, N> for OptionalMergeStrategy
+where
+    N: PartialOrd + Copy
+{
+    fn try_merge(self, master: &mut RangeStep<N>, value: &N) -> Result<()> {
+        let low = master.low.get_or_insert(*value);
+        *low = if *value < *low { *value } else { *low };
+        let high = master.high.get_or_insert(*value);
+        *high = if *value > *high { *value } else { *high };
+        Ok(())
+    }
 }
 
-merge_strategy_for_numbers!(RangeStep for u64, i64, f64, u32, i32, f32,);
-merge_strategy_for_numbers!(Categorical for u64, i64, u32, i32,);
+impl<N> MergeStrategy<Categorical<N>, N> for OptionalMergeStrategy
+where
+    N: Copy + CategoricalType
+{
+    fn try_merge(
+        self,
+        master: &mut Categorical<N>,
+        value: &N
+    ) -> Result<()> {
+        master.push(*value);
+        Ok(())
+    }
+}
 
 impl MergeStrategy<Id, u64> for OptionalMergeStrategy {
     fn try_merge(self, master: &mut Id, candidate: &u64) -> Result<()> {
@@ -527,10 +523,10 @@ pub mod tests {
             .unwrap();
 
         match master {
-            NumberContent::U64(number_content::U64::Range(RangeStep { low, high, step })) => {
-                assert_eq!(low, 0);
-                assert_eq!(high, 15);
-                assert_eq!(step, 1);
+            NumberContent::U64(number_content::U64::Range(RangeStep { low, high, step, .. })) => {
+                assert_eq!(low, Some(0));
+                assert_eq!(high, Some(15));
+                assert_eq!(step, Some(1));
             }
             _ => unreachable!(),
         }
@@ -543,10 +539,10 @@ pub mod tests {
             .unwrap();
 
         match master {
-            NumberContent::I64(number_content::I64::Range(RangeStep { low, high, step })) => {
-                assert_eq!(low, -10);
-                assert_eq!(high, 20);
-                assert_eq!(step, 1);
+            NumberContent::I64(number_content::I64::Range(RangeStep { low, high, step, .. })) => {
+                assert_eq!(low, Some(-10));
+                assert_eq!(high, Some(20));
+                assert_eq!(step, Some(1));
             }
             _ => unreachable!(),
         }
@@ -559,10 +555,10 @@ pub mod tests {
             .unwrap();
 
         match master {
-            NumberContent::F64(number_content::F64::Range(RangeStep { low, high, step })) => {
-                assert!((low - -13.6).abs() < error_margin);
-                assert!((high - 20.6).abs() < error_margin);
-                assert!((step - 1.).abs() < error_margin);
+            NumberContent::F64(number_content::F64::Range(RangeStep { low, high, step , .. })) => {
+                assert!((low.unwrap() - -13.6).abs() < error_margin);
+                assert!((high.unwrap() - 20.6).abs() < error_margin);
+                assert!((step.unwrap() - 1.).abs() < error_margin);
             }
             _ => unreachable!(),
         }
diff --git a/core/src/schema/mod.rs b/core/src/schema/mod.rs
index c83a99f6..a6a458bb 100644
--- a/core/src/schema/mod.rs
+++ b/core/src/schema/mod.rs
@@ -28,7 +28,6 @@ pub mod inference;
 pub use inference::{MergeStrategy, OptionalMergeStrategy, ValueMergeStrategy};
 
 pub mod optionalise;
-pub mod s_override;
 
 pub mod namespace;
 pub use namespace::Namespace;
diff --git a/core/src/schema/s_override.rs b/core/src/schema/s_override.rs
deleted file mode 100644
index 26ec59f0..00000000
--- a/core/src/schema/s_override.rs
+++ /dev/null
@@ -1,392 +0,0 @@
-use crate::schema::FieldRef;
-use crate::schema::{Content, MergeStrategy, ObjectContent, OneOfContent, ValueMergeStrategy};
-use crate::Namespace;
-
-use anyhow::Result;
-use serde_json::Value;
-
-#[derive(Clone, Copy)]
-pub struct DefaultOverrideStrategy<'t> {
-    pub at: &'t FieldRef,
-    pub depth: Option<usize>,
-}
-
-pub trait OverrideStrategy {
-    fn merge(self, ns: &mut Namespace, value: &Value) -> Result<()>;
-    fn delete_from(self, ns: &mut Namespace) -> Result<()>;
-}
-
-impl<'t> OverrideStrategy for DefaultOverrideStrategy<'t> {
-    fn merge(self, ns: &mut Namespace, value: &Value) -> Result<()> {
-        let content = ns.get_s_node_mut(self.at)?;
-        let mut serialized = serde_json::to_value(&content)?;
-        let strategy = ValueMergeStrategy {
-            depth: self.depth.as_ref().map(|depth| *depth as i32),
-            replace: false,
-        };
-        strategy.try_merge(&mut serialized, value)?;
-
-        debug!(
-            "merged value={}",
-            serde_json::to_string(&serialized).unwrap_or_else(|_| "{unknown}".to_string())
-        );
-
-        *content = serde_json::from_value(serialized)?;
-        Ok(())
-    }
-
-    fn delete_from(self, ns: &mut Namespace) -> Result<()> {
-        let parent = self
-            .at
-            .parent()
-            .ok_or_else(|| failed!(target: Release, "attempted to delete a collection"))?;
-        // SAFETY: `last` panics only if `fields` not empty, but guaranteed here
-        let child = self.at.last();
-        let parent_node = ns.get_s_node_mut(&parent)?;
-        match parent_node {
-        Content::OneOf(OneOfContent { variants }) => {
-		let idx: usize = child
-		    .parse()
-		    .map_err(|err| failed!(target: Release, "invalid index: {}", err)).and_then(|idx| {
-			if idx >= variants.len() {
-			    Err(failed!(target: Release, "index {} is out of bounds", idx))
-			} else {
-			    Ok(idx)
-			}
-		    })?;
-		variants.remove(idx);
-		Ok(())
-	    },
-	    Content::Object(ObjectContent { fields, .. }) => {
-		fields
-		    .remove(&child)
-		    .ok_or_else(|| failed!(target: Release, "field '{}' not a member of '{}'", child, parent))?;
-		Ok(())
-	    },
-	    otherwise => Err(failed!(target: Release, "the element referred to by a delete operation needs to be contained inside a 'one_of' or an 'object'; instead we have a '{}'", otherwise))
-	}
-    }
-}
-
-#[cfg(test)]
-pub mod tests {
-    use super::*;
-
-    use crate::schema::tests::USER_NAMESPACE;
-    use crate::schema::{number_content, StringContent};
-    use crate::schema::{ArrayContent, FakerContent, NumberContent, RangeStep};
-
-    #[test]
-    fn override_leaf() {
-        let mut ns = USER_NAMESPACE.clone();
-
-        let field_ref = "users.address.numbers".parse().unwrap();
-
-        let strategy = DefaultOverrideStrategy {
-            at: &field_ref,
-            depth: None,
-        };
-
-        strategy
-            .merge(
-                &mut ns,
-                &json!({
-                        "subtype": "u64",
-                        "range": {
-                "low": 0,
-                "high": 100,
-                "step": 1
-                        }
-                    }),
-            )
-            .unwrap();
-
-        match ns.get_s_node_mut(&field_ref).unwrap() {
-            Content::Number(NumberContent::U64(number_content::U64::Range(RangeStep {
-                low,
-                high,
-                step,
-            }))) => {
-                assert_eq!(*low, 0);
-                assert_eq!(*high, 100);
-                assert_eq!(*step, 1);
-            }
-            _ => unreachable!(),
-        }
-    }
-
-    #[test]
-    fn override_knows_to_replace_string_subtype() {
-        let mut ns = USER_NAMESPACE.clone();
-
-        let field_ref = "users.address.country".parse().unwrap();
-
-        let strategy = DefaultOverrideStrategy {
-            at: &field_ref,
-            depth: None,
-        };
-
-        strategy
-            .merge(
-                &mut ns,
-                &json!({
-                "faker": {
-                "generator": "name"
-                }
-                    }),
-            )
-            .unwrap();
-
-        match ns.get_s_node_mut(&field_ref).unwrap() {
-            Content::String(StringContent::Faker(FakerContent {
-                generator,
-                args: _,
-                locales: _,
-            })) => {
-                assert_eq!(generator.as_str(), "name");
-                //assert!(args.is_empty())
-                //TODO
-            }
-            _ => unreachable!(),
-        }
-    }
-
-    #[test]
-    fn override_knows_to_replace_number_subtype() {
-        let mut ns = USER_NAMESPACE.clone();
-
-        let field_ref = "users.user_id".parse().unwrap();
-
-        let strategy = DefaultOverrideStrategy {
-            at: &field_ref,
-            depth: None,
-        };
-
-        strategy
-            .merge(
-                &mut ns,
-                &json!({
-                        "subtype": "i64",
-                "constant": -100
-                    }),
-            )
-            .unwrap();
-
-        match ns.get_s_node_mut(&field_ref).unwrap() {
-            Content::Number(NumberContent::I64(number_content::I64::Constant(-100))) => {}
-            _ => unreachable!(),
-        }
-    }
-
-    #[test]
-    fn override_array() {
-        let mut ns = USER_NAMESPACE.clone();
-
-        let field_ref = "users.friends".parse().unwrap();
-
-        let strategy = DefaultOverrideStrategy {
-            at: &field_ref,
-            depth: None,
-        };
-
-        strategy
-            .merge(
-                &mut ns,
-                &json!({
-                        "length": {
-                "type": "number",
-                "subtype": "u64",
-                "constant": 100
-                },
-                    "content": {
-                    "variants": [ {
-                        "type": "string",
-                        "pattern": "new variant!"
-                    } ]
-                    }
-                        }),
-            )
-            .unwrap();
-
-        match ns.get_s_node_mut(&field_ref).unwrap() {
-            Content::Array(ArrayContent {
-                length: box Content::Number(NumberContent::U64(number_content::U64::Constant(100))),
-                content,
-            }) => match content.as_mut() {
-                Content::OneOf(one_of_content) => {
-                    assert_eq!(one_of_content.iter().count(), 3);
-                }
-                _ => unreachable!(),
-            },
-            _ => unreachable!(),
-        }
-    }
-
-    #[test]
-    fn override_replace() {
-        let mut ns = USER_NAMESPACE.clone();
-
-        let field_ref = "users.friends".parse().unwrap();
-
-        let strategy = DefaultOverrideStrategy {
-            at: &field_ref,
-            depth: Some(0),
-        };
-
-        strategy
-            .merge(
-                &mut ns,
-                &json!({
-                "type": "string",
-                "pattern": "no friends here"
-                    }),
-            )
-            .unwrap();
-
-        match ns.get_s_node_mut(&field_ref).unwrap() {
-            Content::String(StringContent::Pattern(regex)) => {
-                assert_eq!(regex.to_string(), "no friends here".to_string());
-            }
-            _ => unreachable!(),
-        }
-    }
-
-    #[test]
-    fn override_schema_no_key() {
-        let mut ns = USER_NAMESPACE.clone();
-
-        let field_ref = "users.i.do.not.exist".parse().unwrap();
-
-        let strategy = DefaultOverrideStrategy {
-            at: &field_ref,
-            depth: None,
-        };
-
-        assert!(strategy.merge(&mut ns, &Value::default()).is_err());
-    }
-
-    #[test]
-    fn override_schema_mismatched_types() {
-        let mut ns = USER_NAMESPACE.clone();
-
-        let field_ref = "users.address.postcode".parse().unwrap(); // a string in `ns`
-
-        let strategy = DefaultOverrideStrategy {
-            at: &field_ref,
-            depth: None,
-        };
-
-        let res = strategy.merge(
-            &mut ns,
-            &json!({
-                "range": {
-                "low": 100
-                }
-            }),
-        );
-
-        assert!(res.is_err());
-    }
-
-    #[test]
-    fn override_schema_array_variant() {
-        let mut ns = USER_NAMESPACE.clone();
-
-        let field_ref = "users.friends.content.0".parse().unwrap(); // a string in `ns`
-
-        let strategy = DefaultOverrideStrategy {
-            at: &field_ref,
-            depth: Some(0),
-        };
-
-        strategy
-            .merge(
-                &mut ns,
-                &json!({
-                    "type": "number",
-                    "subtype": "u64",
-                    "constant": 10
-                }),
-            )
-            .unwrap();
-
-        match ns.get_s_node_mut(&field_ref).unwrap() {
-            Content::Number(NumberContent::U64(number_content::U64::Constant(10))) => {}
-            _ => unreachable!(),
-        }
-    }
-
-    #[test]
-    fn override_schema_array_out_of_bounds() {
-        let mut ns = USER_NAMESPACE.clone();
-
-        let field_ref = "users.friends.10".parse().unwrap();
-
-        let strategy = DefaultOverrideStrategy {
-            at: &field_ref,
-            depth: None,
-        };
-
-        assert!(strategy.merge(&mut ns, &Value::default()).is_err());
-    }
-
-    #[test]
-    fn override_schema_array_not_an_integer() {
-        let mut ns = USER_NAMESPACE.clone();
-
-        let field_ref = "users.friends.not_an_integer".parse().unwrap();
-
-        let strategy = DefaultOverrideStrategy {
-            at: &field_ref,
-            depth: None,
-        };
-
-        assert!(strategy.merge(&mut ns, &Value::default()).is_err());
-    }
-
-    #[test]
-    fn override_delete_from_object() {
-        let mut ns = USER_NAMESPACE.clone();
-
-        let field_ref = "users.friends".parse().unwrap();
-
-        let strategy = DefaultOverrideStrategy {
-            at: &field_ref,
-            depth: None,
-        };
-
-        strategy.delete_from(&mut ns).unwrap();
-
-        assert!(ns.get_s_node_mut(&field_ref).is_err())
-    }
-
-    #[test]
-    fn override_delete_from_one_of() {
-        let mut ns = USER_NAMESPACE.clone();
-
-        let field_ref = "users.friends.content.1".parse().unwrap();
-
-        let strategy = DefaultOverrideStrategy {
-            at: &field_ref,
-            depth: None,
-        };
-
-        strategy.delete_from(&mut ns).unwrap();
-
-        assert!(ns.get_s_node_mut(&field_ref).is_err())
-    }
-
-    #[test]
-    fn override_cannot_delete_top_level() {
-        let mut ns = USER_NAMESPACE.clone();
-
-        let field_ref = "users".parse().unwrap();
-
-        let strategy = DefaultOverrideStrategy {
-            at: &field_ref,
-            depth: None,
-        };
-
-        assert!(strategy.delete_from(&mut ns).is_err());
-    }
-}
diff --git a/docs/docs/content/number.md b/docs/docs/content/number.md
index bcc5b6f3..fc702955 100644
--- a/docs/docs/content/number.md
+++ b/docs/docs/content/number.md
@@ -22,13 +22,80 @@ the best value based on the value of other parameters. But it may be necessary
 to set it manually in situations where the data sink only accepts certain
 widths (e.g. postgres).
 
+### Defaults
+
+If no variant (such as `range`, `constant`, etc.) is specified, `number`
+will have different default behavior based on the value of `"subtype"`.
+
+- For integer subtypes (`i32`, `u32`, etc.): `number` will default to generating one of the integers in the
+  representable range of the subtype.
+- For float subtypes (`f32`, `f64`): `number` will default to generating from the semi-open interval `[0, 1)`.
+
+#### Example
+```json synth
+{
+  "type": "number",
+  "subtype": "i32"
+}
+```
+
+#### Example
+```json synth
+{
+  "type": "number",
+  "subtype": "f32"
+}
+```
+
 ## range
 
-Defines a range over a semi-open interval `[low, high)` with a specified `step`. Values are generated by sampling a
-random non-negative integer `n` and computing `low + n*step`.
+Generates numbers of a particular type contained in a specified interval.
+
+### Parameters
+
+- `"low"` (optional, number): the lower bound of the interval
+- `"high"` (optional, number): the upper bound of the interval
+- `"step"` (optional, number): force alignment of generated numbers on multiples
+  of `"step"` from the value of `"low"`.
+- `"include_low"` (optional, bool): whether to include the specified lower bound
+  in the range. Defaults to `true`.
+- `"include_high"` (optional, bool): whether to include the specified upper
+  bound in the range. Defaults to `false`.
+
+#### Example
+
+This generates one of the integers `0, 3, 6, 9`.
+
+```json synth
+{
+  "type": "number",
+  "range": {
+      "low": 0,
+      "high": 10,
+      "step": 3
+  }
+}
+```
+
+#### Example
+
+This generates one integer between `0` (included) and `122` (included).
+
+```json synth
+{
+  "type": "number",
+  "range": {
+      "high": 122,         // the age of the oldest recorded person
+      "include_high": true      
+  }
+}
+```
 
 #### Example
 
+This generates one floating-point number between `-273.15` (included)
+and `15000000.0` (excluded) with an approximate alignment to the second decimal.
+
 ```json synth
 {
   "type": "number",
@@ -40,6 +107,36 @@ random non-negative integer `n` and computing `low + n*step`.
 }
 ```
 
+### Defaults
+
+For values of `"subtype"` belonging to the integer class (`i32`, `u32`, etc.),
+the parameters `"low"`, `"high"` and `"step"` default to the following values if
+not specified explicitly:
+
+- `"low"`: the minimum representable integer in the subtype
+- `"high"`: the maximum representable integer in the subtype
+- `"step"`: the integer `1`
+
+#### Example
+
+Not specifying any of `"low"`, `"high"` is equivalent to setting the bounds to
+the minimum and maximum representable integers in the subtype.
+
+```json synth
+{
+  "type": "number",
+  "subtype": "i32",
+  "range": {}
+}
+```
+
+For values of `"subtype"` belonging to the float class (`f32`, `f64`, etc.), the
+parameters `"low"`, `"high"` default to the following values if not specified
+explicitly:
+
+- `"low"`: the floating-point number `0.`
+- `"high"`: the floating-point number `1.`
+
 ## constant
 
 A constant number type. This will always evaluate to the same number.
diff --git a/examples/bank/bank_db/users.json b/examples/bank/bank_db/users.json
index 3bb69493..c1f18588 100644
--- a/examples/bank/bank_db/users.json
+++ b/examples/bank/bank_db/users.json
@@ -73,4 +73,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/nix/overlay.nix b/nix/overlay.nix
index 59a7d1b0..ef9af0e4 100644
--- a/nix/overlay.nix
+++ b/nix/overlay.nix
@@ -4,7 +4,7 @@
 self: super: {
   synthPackages = {
     rustToolchain = super.rustChannelOf {
-      date = "2021-09-01";
+      date = "2021-09-15";
       channel = "nightly";
     };
 
diff --git a/synth/src/cli/import_utils.rs b/synth/src/cli/import_utils.rs
index 9581dd43..5a320582 100644
--- a/synth/src/cli/import_utils.rs
+++ b/synth/src/cli/import_utils.rs
@@ -149,11 +149,7 @@ impl<T: RelationalDataSource + DataSource> TryFrom<(&T, Vec<ColumnInfo>)> for Co
 
         Ok(Collection {
             collection: Content::Array(ArrayContent {
-                length: Box::new(Content::Number(NumberContent::U64(U64::Range(RangeStep {
-                    low: 1,
-                    high: 2,
-                    step: 1,
-                })))),
+                length: Box::new(Content::Number(NumberContent::U64(U64::Range(RangeStep::new(1, 2, 1))))),
                 content: Box::new(Content::Object(collection)),
             }),
         })
diff --git a/synth/src/cli/mongo.rs b/synth/src/cli/mongo.rs
index fed21cd3..1dc8d98c 100644
--- a/synth/src/cli/mongo.rs
+++ b/synth/src/cli/mongo.rs
@@ -117,11 +117,7 @@ fn doc_to_content(doc: &Document) -> Content {
 
 fn bson_to_content(bson: &Bson) -> Content {
     match bson {
-        Bson::Double(d) => Content::Number(NumberContent::F64(F64::Range(RangeStep {
-            low: *d,
-            high: *d + 1.0,
-            step: 0.1,
-        }))),
+        Bson::Double(d) => Content::Number(NumberContent::F64(F64::Range(RangeStep::new(*d, *d + 1., 0.1)))),
         Bson::String(_) => Content::String(StringContent::default()),
         Bson::Array(array) => {
             let length = Content::Number(NumberContent::U64(U64::Constant(array.len() as u64)));
@@ -144,16 +140,8 @@ fn bson_to_content(bson: &Bson) -> Content {
         Bson::JavaScriptCodeWithScope(_) => {
             Content::String(StringContent::Categorical(Categorical::default()))
         }
-        Bson::Int32(i) => Content::Number(NumberContent::I64(I64::Range(RangeStep {
-            low: *i as i64,
-            high: *i as i64 + 1,
-            step: 1,
-        }))),
-        Bson::Int64(i) => Content::Number(NumberContent::I64(I64::Range(RangeStep {
-            low: *i,
-            high: *i + 1,
-            step: 1,
-        }))),
+        Bson::Int32(i) => Content::Number(NumberContent::I64(I64::Range(RangeStep::new(*i as i64, *i as i64 + 1, 1)))),
+        Bson::Int64(i) => Content::Number(NumberContent::I64(I64::Range(RangeStep::new(*i, *i + 1, 1)))),
         Bson::DateTime(_) => Content::String(StringContent::DateTime(DateTimeContent {
             format: "".to_string(),
             type_: ChronoValueType::DateTime,
diff --git a/synth/src/datasource/mysql_datasource.rs b/synth/src/datasource/mysql_datasource.rs
index 6cbe454d..a72cc7f6 100644
--- a/synth/src/datasource/mysql_datasource.rs
+++ b/synth/src/datasource/mysql_datasource.rs
@@ -163,23 +163,11 @@ impl RelationalDataSource for MySqlDataSource {
                 ))
             }
             "int" | "integer" | "tinyint" | "smallint" | "mediumint" | "bigint" => {
-                Content::Number(NumberContent::I64(I64::Range(RangeStep {
-                    low: 0,
-                    high: 1,
-                    step: 1,
-                })))
+                Content::Number(NumberContent::I64(I64::Range(RangeStep::default())))
             }
-            "serial" => Content::Number(NumberContent::U64(U64::Range(RangeStep {
-                low: 0,
-                high: 1,
-                step: 1,
-            }))),
+            "serial" => Content::Number(NumberContent::U64(U64::Range(RangeStep::default()))),
             "float" | "double" | "numeric" | "decimal" => {
-                Content::Number(NumberContent::F64(F64::Range(RangeStep {
-                    low: 0.0,
-                    high: 1.0,
-                    step: 0.1, //todo
-                })))
+                Content::Number(NumberContent::F64(F64::Range(RangeStep::default())))
             }
             "timestamp" => Content::String(StringContent::DateTime(DateTimeContent {
                 format: "".to_string(), // todo
diff --git a/synth/src/datasource/postgres_datasource.rs b/synth/src/datasource/postgres_datasource.rs
index 9baef80b..7362147e 100644
--- a/synth/src/datasource/postgres_datasource.rs
+++ b/synth/src/datasource/postgres_datasource.rs
@@ -176,36 +176,12 @@ impl RelationalDataSource for PostgresDataSource {
                     RegexContent::pattern(pattern).context("pattern will always compile")?,
                 ))
             }
-            "int2" => Content::Number(NumberContent::I64(I64::Range(RangeStep {
-                low: 0,
-                high: 1,
-                step: 1,
-            }))),
-            "int4" => Content::Number(NumberContent::I32(I32::Range(RangeStep {
-                low: 0,
-                high: 1,
-                step: 1,
-            }))),
-            "int8" => Content::Number(NumberContent::I64(I64::Range(RangeStep {
-                low: 0,
-                high: 1,
-                step: 1,
-            }))),
-            "float4" => Content::Number(NumberContent::F32(F32::Range(RangeStep {
-                low: 0.0,
-                high: 1.0,
-                step: 0.1, //todo
-            }))),
-            "float8" => Content::Number(NumberContent::F64(F64::Range(RangeStep {
-                low: 0.0,
-                high: 1.0,
-                step: 0.1, //todo
-            }))),
-            "numeric" => Content::Number(NumberContent::F64(F64::Range(RangeStep {
-                low: 0.0,
-                high: 1.0,
-                step: 0.1, //todo
-            }))),
+            "int2" => Content::Number(NumberContent::I64(I64::Range(RangeStep::default()))),
+            "int4" => Content::Number(NumberContent::I32(I32::Range(RangeStep::default()))),
+            "int8" => Content::Number(NumberContent::I64(I64::Range(RangeStep::default()))),
+            "float4" => Content::Number(NumberContent::F32(F32::Range(RangeStep::default()))),
+            "float8" => Content::Number(NumberContent::F64(F64::Range(RangeStep::default()))),
+            "numeric" => Content::Number(NumberContent::F64(F64::Range(RangeStep::default()))),
             "timestamptz" => Content::String(StringContent::DateTime(DateTimeContent {
                 format: "".to_string(), // todo
                 type_: ChronoValueType::DateTime,


Test patch:
diff --git a/synth/testing_harness/mysql/hospital_master/doctors.json b/synth/testing_harness/mysql/hospital_master/doctors.json
index 6e67eeb3..cc532627 100644
--- a/synth/testing_harness/mysql/hospital_master/doctors.json
+++ b/synth/testing_harness/mysql/hospital_master/doctors.json
@@ -37,9 +37,8 @@
           "weight": 1.0,
           "type": "number",
           "range": {
-            "low": 0,
-            "high": 14,
-            "step": 1
+            "low": 1,
+            "high": 14
           },
           "subtype": "i64"
         },
@@ -69,4 +68,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/synth/testing_harness/mysql/hospital_master/patients.json b/synth/testing_harness/mysql/hospital_master/patients.json
index 1619a732..138c34ea 100644
--- a/synth/testing_harness/mysql/hospital_master/patients.json
+++ b/synth/testing_harness/mysql/hospital_master/patients.json
@@ -51,9 +51,8 @@
           "weight": 1.0,
           "type": "number",
           "range": {
-            "low": 0,
-            "high": 20,
-            "step": 1
+            "low": 1,
+            "high": 20
           },
           "subtype": "i64"
         },
@@ -111,4 +110,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/synth/testing_harness/postgres/hospital_data_generated_master.json b/synth/testing_harness/postgres/hospital_data_generated_master.json
index 808185d0..d89478cb 100644
--- a/synth/testing_harness/postgres/hospital_data_generated_master.json
+++ b/synth/testing_harness/postgres/hospital_data_generated_master.json
@@ -1,1 +1,1 @@
-{"doctors":[{"date_joined":"2017-12-25","hospital_id":0,"id":1,"name":null}],"hospitals":[{"address":"dTsr3zDyuf04ccmf55Wkr5LBWyGFzffnId25CeaBDJLYd15KF2uRb2noqulsLtX6HMlrAVC6HKHJqsUYlW4ykDizAFLHsbCXU7pJmesZRDNYcFy43p7G1e11ODKsFh8e","hospital_name":"NBQmRAXxBg9oFdaBL1tWkbY8jRmG6xjnchpv08FQOovJDr0nnq4m8Er3IxAhd0ccccvZciPCulne8nuQX0j8ddHUlNrw6dw6GpIWOLEDLCcJiZDj0fkbB9iV1guzLlgxS8zTqxpJM5Q6CBF1WKhZ7RYWt4a5NwKgihcWiKlU6NHDWh","id":1},{"address":null,"hospital_name":"2nhVel8NQPJ0PUL228jE0SCVmYnnuXwQqX5DJTAYdvXxD53ZAGMjFj6K46UIp8CF9xkuGd3DhU9RJyfK0myw5vJXdAqU9Ns9ycOsC","id":2},{"address":null,"hospital_name":"Kbm9rsF3f0q1zhjUz","id":3},{"address":"wK2PCkqe45tki0A9IX6lWRkQXUjy6rW6HqgDn1kX3mmKXj7HhjLDRRbw3vmLIlrw26rYtdNMeWlmayTleVzvHPCjZFTJm8SzGOe0iTASsVtZjQShDtfYkVEAZrDoCrO6VK5nnyXvUk9SSNyc58DZ8L","hospital_name":"2YC5ZY1WlGfrLO52adbvSgFUZCPhUaYCPT3QoQYHEA55ziZvgWRT78Tjn4j8m1zEG0vZJu0zxA9dIZCrd8NltfLA3xFy2ctNTT4n","id":4},{"address":null,"hospital_name":"pQZkiB6FADYFNtRzvY0diniut4z3JG270eww5CvhiQwG4Bj","id":5},{"address":null,"hospital_name":null,"id":6},{"address":"GQbZoTdHui7EZTqVUW3snRjy6esftm","hospital_name":null,"id":7}],"patients":[{"address":"T7Ud9qyEXSsQukTjLv3ptEn4sPadtrCXwxo96964WKpe4QIB9K1tqIcYVYm5jcHw5QDcYmtL42DAJLNjJgIlj1lNKFrZn8OkcSMfikcRDP1xGRiRMSVPMciC2Mq1tndRT2qvba9dmqzaF6fG7zwOy7C1Kg815SulhgcC9RxhAwcdJDjznRamVPxCBCdT0YrhTGSZIfKDLedqdUs9BoF","date_joined":null,"doctor_id":null,"id":1,"name":null,"phone":null,"ssn":null},{"address":"yCMTxtQOe55Nmk5mAcLOFK9WdeZiXnQQVqdRCNrlfPg4tg9JRTryU0BThh5BGZoIcRbmbySmrkAYSA8jIgZKoDvlacYKDmbnUErbwg3xRbCKEWZ8CxUXbU1Sv5OQFnZbRKmjAwVeqRSSJ7AUihkPesQsbjhY7sGRBVrnQMrikzZhKSxkeximiNZg2I7LAQHx0ojaw9OmdMYMsxVcJBX61fB1NbUrBZ63fAOpaPgtC","date_joined":"2018-01-12","doctor_id":11,"id":2,"name":null,"phone":null,"ssn":null},{"address":"MmhMZMRnBkNtw5h9YfxQXtVKixIucvh9kk4v3Fmuw6q0QYokvTXyIKXv2pT8VlCGqZ74SRf646sr0FEYve50TACTIgzdVLh79w64eIadVGK7QWJ6kpuvRtdMNIBJNDzF0VqqtBX7sLdKiwTelwQGgPvMHolvhTpQme5SnyxmgzKusz01bMS9l3nx3dD4fd5exyQG1CZAblSsXgsMGYoPkX8XTPALAUD63qxYahTbw9V57HteORc","date_joined":null,"doctor_id":8,"id":3,"name":null,"phone":"KSsmjS9rgElCUN16Unw","ssn":"KFEBxm5Y"}]}
+{"doctors":[{"date_joined":"2017-12-25","hospital_id":1,"id":1,"name":null}],"hospitals":[{"address":"dTsr3zDyuf04ccmf55Wkr5LBWyGFzffnId25CeaBDJLYd15KF2uRb2noqulsLtX6HMlrAVC6HKHJqsUYlW4ykDizAFLHsbCXU7pJmesZRDNYcFy43p7G1e11ODKsFh8e","hospital_name":"NBQmRAXxBg9oFdaBL1tWkbY8jRmG6xjnchpv08FQOovJDr0nnq4m8Er3IxAhd0ccccvZciPCulne8nuQX0j8ddHUlNrw6dw6GpIWOLEDLCcJiZDj0fkbB9iV1guzLlgxS8zTqxpJM5Q6CBF1WKhZ7RYWt4a5NwKgihcWiKlU6NHDWh","id":1},{"address":null,"hospital_name":"2nhVel8NQPJ0PUL228jE0SCVmYnnuXwQqX5DJTAYdvXxD53ZAGMjFj6K46UIp8CF9xkuGd3DhU9RJyfK0myw5vJXdAqU9Ns9ycOsC","id":2},{"address":null,"hospital_name":"Kbm9rsF3f0q1zhjUz","id":3},{"address":"wK2PCkqe45tki0A9IX6lWRkQXUjy6rW6HqgDn1kX3mmKXj7HhjLDRRbw3vmLIlrw26rYtdNMeWlmayTleVzvHPCjZFTJm8SzGOe0iTASsVtZjQShDtfYkVEAZrDoCrO6VK5nnyXvUk9SSNyc58DZ8L","hospital_name":"2YC5ZY1WlGfrLO52adbvSgFUZCPhUaYCPT3QoQYHEA55ziZvgWRT78Tjn4j8m1zEG0vZJu0zxA9dIZCrd8NltfLA3xFy2ctNTT4n","id":4},{"address":null,"hospital_name":"pQZkiB6FADYFNtRzvY0diniut4z3JG270eww5CvhiQwG4Bj","id":5},{"address":null,"hospital_name":null,"id":6},{"address":"GQbZoTdHui7EZTqVUW3snRjy6esftm","hospital_name":null,"id":7}],"patients":[{"address":"T7Ud9qyEXSsQukTjLv3ptEn4sPadtrCXwxo96964WKpe4QIB9K1tqIcYVYm5jcHw5QDcYmtL42DAJLNjJgIlj1lNKFrZn8OkcSMfikcRDP1xGRiRMSVPMciC2Mq1tndRT2qvba9dmqzaF6fG7zwOy7C1Kg815SulhgcC9RxhAwcdJDjznRamVPxCBCdT0YrhTGSZIfKDLedqdUs9BoF","date_joined":null,"doctor_id":null,"id":1,"name":null,"phone":null,"ssn":null},{"address":"yCMTxtQOe55Nmk5mAcLOFK9WdeZiXnQQVqdRCNrlfPg4tg9JRTryU0BThh5BGZoIcRbmbySmrkAYSA8jIgZKoDvlacYKDmbnUErbwg3xRbCKEWZ8CxUXbU1Sv5OQFnZbRKmjAwVeqRSSJ7AUihkPesQsbjhY7sGRBVrnQMrikzZhKSxkeximiNZg2I7LAQHx0ojaw9OmdMYMsxVcJBX61fB1NbUrBZ63fAOpaPgtC","date_joined":"2018-01-12","doctor_id":13,"id":2,"name":null,"phone":"sTnaul","ssn":null},{"address":"MmhMZMRnBkNtw5h9YfxQXtVKixIucvh9kk4v3Fmuw6q0QYokvTXyIKXv2pT8VlCGqZ74SRf646sr0FEYve50TACTIgzdVLh79w64eIadVGK7QWJ6kpuvRtdMNIBJNDzF0VqqtBX7sLdKiwTelwQGgPvMHolvhTpQme5SnyxmgzKusz01bMS9l3nx3dD4fd5exyQG1CZAblSsXgsMGYoPkX8XTPALAUD63qxYahTbw9V57HteORc","date_joined":null,"doctor_id":10,"id":3,"name":"d64rAV4cHkjK89NJJdoZIVN03XgeKFEBxm5Y3t7Es41y6pykOZa1uvH5juNQUrbr4xZeQleBUijhFjRTZUdIyTC5wNhwO0ICtfIwdFgpGuigJs80myHxbj9RL","phone":null,"ssn":null}]}
diff --git a/synth/testing_harness/postgres/hospital_master/doctors.json b/synth/testing_harness/postgres/hospital_master/doctors.json
index ed778555..9180553c 100644
--- a/synth/testing_harness/postgres/hospital_master/doctors.json
+++ b/synth/testing_harness/postgres/hospital_master/doctors.json
@@ -37,9 +37,8 @@
           "weight": 1.0,
           "type": "number",
           "range": {
-            "low": 0,
-            "high": 12,
-            "step": 1
+            "low": 1,
+            "high": 12
           },
           "subtype": "i32"
         },
@@ -69,4 +68,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/synth/testing_harness/postgres/hospital_master/patients.json b/synth/testing_harness/postgres/hospital_master/patients.json
index 9d35c831..40ce9e6b 100644
--- a/synth/testing_harness/postgres/hospital_master/patients.json
+++ b/synth/testing_harness/postgres/hospital_master/patients.json
@@ -51,9 +51,8 @@
           "weight": 1.0,
           "type": "number",
           "range": {
-            "low": 0,
-            "high": 19,
-            "step": 1
+            "low": 1,
+            "high": 19
           },
           "subtype": "i32"
         },
@@ -111,4 +110,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}