Repository: shuttle-hq/synth (rust)

Issue:
bug: generating int2 for postgres gives 'incorrect binary data format' error
**Describe the bug**
Trying to generate a number for a postgres `int2` column gives an 'incorrect binary data format' error.

**To Reproduce**
Steps to reproduce the behavior:
1. Schema (postgres)
```sql
CREATE TABLE test
(
	age int2
)
```
2. Schema (Synth)
```json
{
  "type": "array",
  "length": 1,
  "content": {
    "type": "object",
    "age": {
      "type": "number",
      "subtype": "i64",
      "constant": 5
    }
  }
}
```
3. See error
```
Error: At namespace "int2"

Caused by:
    0: Failed to insert data for collection test
    1: One or more database inserts failed: error returned from database: incorrect binary data format in bind parameter 1: incorrect binary data format in bind parameter 1
```
4. Error (postgres logs)
```
2021-12-23 07:33:04.840 UTC [89] ERROR:  incorrect binary data format in bind parameter 1
2021-12-23 07:33:04.840 UTC [89] CONTEXT:  unnamed portal parameter $1
2021-12-23 07:33:04.840 UTC [89] STATEMENT:  INSERT INTO test (age) VALUES
	($1);
```

**Expected behavior**
According to the [postgres integration](https://www.getsynth.com/docs/integrations/postgres) page the generation should succeed. On a technical level, the integration page is wrong as an `int2` is equivalent to an `i16` which Synth does not have.

**Environment (please complete the following information):**
 - OS: Linux
 - Version: 0.6.2

**Additional context**
A successful fix should restore this [e2e test](https://github.com/getsynth/synth/pull/268/files#diff-5c7f226dd02b2a0c9d5768d724c00b4eee2bbadef264763ec45e8264e54fcfcfR14) and possibly also update the integration page



Discussion:

Patch:
diff --git a/core/src/graph/number.rs b/core/src/graph/number.rs
--- a/core/src/graph/number.rs
+++ b/core/src/graph/number.rs
@@ -51,6 +51,7 @@ macro_rules! any_range_int_impl {
     }
 }
 
+any_range_int_impl! { u16 }
 any_range_int_impl! { u32 }
 any_range_int_impl! { u64 }
 
@@ -218,6 +219,7 @@ macro_rules! standard_int_range_step_impl {
     }
 }
 
+standard_int_range_step_impl! { i16, i32, u16}
 standard_int_range_step_impl! { i32, i64, u32 }
 standard_int_range_step_impl! { u32, u32, u32 }
 standard_int_range_step_impl! { i64, i128, u64 }
@@ -472,6 +474,12 @@ number_node!(
         F32Range<StandardFloatRangeStep<f32>> as range,
         F32Constant as constant,,,
     ) for f32,
+    RandomI16 (
+        I16Range<StandardIntRangeStep<u16, i32>> as range,
+        I16Constant as constant,
+        I16Categorical as categorical,
+        Incrementing as incrementing,
+    ) for i16,
 );
 
 #[cfg(test)]
diff --git a/core/src/schema/content/array.rs b/core/src/schema/content/array.rs
--- a/core/src/schema/content/array.rs
+++ b/core/src/schema/content/array.rs
@@ -103,6 +103,15 @@ impl<'de> Deserialize<'de> for ArrayContent {
                             r.low .unwrap_or_default() .into(),
                         ).map_err(A::Error::custom)?
                     },
+                    Content::Number(NumberContent::I16(number_content::I16::Range(r))) => {
+                        if r.high.is_none() {
+                            return Err(A::Error::custom("missing high value for array length range"));
+                        }
+
+                        is_positive(
+                            r.low .unwrap_or_default() .into(),
+                        ).map_err(A::Error::custom)?
+                    },
                     Content::SameAs(_) => {},
                     Content::Null(_) => return Err(de::Error::custom("array length is missing. Try adding '\"length\": [number]' to the array type where '[number]' is a positive integer")),
                     Content::Empty(_) => return Err(de::Error::custom("array length is not a constant or number type. Try replacing the '\"length\": {}' with '\"length\": [number]' where '[number]' is a positive integer")),
diff --git a/core/src/schema/content/number.rs b/core/src/schema/content/number.rs
--- a/core/src/schema/content/number.rs
+++ b/core/src/schema/content/number.rs
@@ -4,7 +4,7 @@ use std::hash::{Hash, Hasher};
 
 use super::Categorical;
 
-use crate::graph::number::{RandomF32, RandomI32, RandomU32};
+use crate::graph::number::{RandomF32, RandomI16, RandomI32, RandomU32};
 use serde::{
     de::{Deserialize, Deserializer},
     ser::Serializer,
@@ -209,6 +209,7 @@ impl NumberContent {
         match self {
             NumberContent::U32(_) => Ok(Self::u32_default_id()),
             NumberContent::U64(_) => Ok(Self::u64_default_id()),
+            NumberContent::I16(_) => Ok(Self::i16_default_id()),
             NumberContent::I32(_) => Ok(Self::i32_default_id()),
             NumberContent::I64(_) => Ok(Self::i64_default_id()),
             NumberContent::F64(_) => bail!("could not transmute f64 into id"),
@@ -224,6 +225,10 @@ impl NumberContent {
         NumberContent::U64(number_content::U64::Id(Id::default()))
     }
 
+    pub fn i16_default_id() -> Self {
+        NumberContent::I16(number_content::I16::Id(Id::default()))
+    }
+
     pub fn i32_default_id() -> Self {
         NumberContent::I32(number_content::I32::Id(Id::default()))
     }
@@ -371,7 +376,7 @@ macro_rules! derive_hash {
     };
 }
 
-derive_hash!(i32, u32, i64, u64, f32, f64);
+derive_hash!(i16, i32, u32, i64, u64, f32, f64);
 
 number_content!(
     #[derive(PartialEq, Hash)]
@@ -389,6 +394,13 @@ number_content!(
         Id(crate::schema::Id<u64>),
     },
     #[derive(PartialEq, Hash)]
+    i16[is_i16, default_i16_range] as I16 {
+        Range(RangeStep<i16>),
+        Categorical(Categorical<i16>),
+        Constant(i16),
+        Id(crate::schema::Id<i16>),
+    },
+    #[derive(PartialEq, Hash)]
     i32[is_i32, default_i32_range] as I32 {
         Range(RangeStep<i32>),
         Categorical(Categorical<i32>),
@@ -482,6 +494,19 @@ impl Compile for NumberContent {
                 };
                 random_f32.into()
             }
+            Self::I16(i16_content) => {
+                let random_i16 = match i16_content {
+                    number_content::I16::Range(range) => RandomI16::range(*range)?,
+                    number_content::I16::Categorical(categorical_content) => {
+                        RandomI16::categorical(categorical_content.clone())
+                    }
+                    number_content::I16::Constant(val) => RandomI16::constant(*val),
+                    number_content::I16::Id(id) => {
+                        RandomI16::incrementing(Incrementing::new_at(id.start_at.unwrap_or(1)))
+                    }
+                };
+                random_i16.into()
+            }
         };
         Ok(Graph::Number(number_node))
     }
diff --git a/core/src/schema/inference/mod.rs b/core/src/schema/inference/mod.rs
--- a/core/src/schema/inference/mod.rs
+++ b/core/src/schema/inference/mod.rs
@@ -15,7 +15,7 @@ use super::{
     Content, DateTimeContent, Id, NumberContent, NumberKindExt, ObjectContent, OneOfContent,
     RangeStep, StringContent, ValueKindExt,
 };
-use crate::graph::prelude::content::number_content::{I32, I64};
+use crate::graph::prelude::content::number_content::{I16, I32, I64};
 use crate::schema::UniqueContent;
 use num::Zero;
 
@@ -324,6 +324,17 @@ impl MergeStrategy<number_content::F32, f32> for OptionalMergeStrategy {
     }
 }
 
+impl MergeStrategy<number_content::I16, i16> for OptionalMergeStrategy {
+    fn try_merge(self, master: &mut number_content::I16, candidate: &i16) -> Result<()> {
+        match master {
+            number_content::I16::Range(range) => self.try_merge(range, candidate),
+            number_content::I16::Categorical(cat) => self.try_merge(cat, candidate),
+            number_content::I16::Constant(cst) => self.try_merge(cst, candidate),
+            I16::Id(id) => self.try_merge(id, candidate),
+        }
+    }
+}
+
 impl MergeStrategy<NumberContent, Number> for OptionalMergeStrategy {
     fn try_merge(self, master: &mut NumberContent, value: &Number) -> Result<()> {
         match master {
@@ -372,6 +383,13 @@ impl MergeStrategy<NumberContent, Number> for OptionalMergeStrategy {
                     todo!()
                 }
             }
+            NumberContent::I16(i16_content) => {
+                if let Some(n) = value.as_i64() {
+                    self.try_merge(i16_content, &(n as i16))
+                } else {
+                    todo!()
+                }
+            }
         }
     }
 }
diff --git a/synth/src/datasource/postgres_datasource.rs b/synth/src/datasource/postgres_datasource.rs
--- a/synth/src/datasource/postgres_datasource.rs
+++ b/synth/src/datasource/postgres_datasource.rs
@@ -12,7 +12,7 @@ use sqlx::postgres::{PgColumn, PgPoolOptions, PgRow, PgTypeInfo, PgTypeKind};
 use sqlx::{Column, Executor, Pool, Postgres, Row, TypeInfo};
 use std::collections::BTreeMap;
 use std::convert::TryFrom;
-use synth_core::schema::number_content::{F32, F64, I32, I64};
+use synth_core::schema::number_content::{F32, F64, I16, I32, I64};
 use synth_core::schema::{
     ArrayContent, BoolContent, Categorical, ChronoValue, ChronoValueAndFormat, ChronoValueType,
     DateTimeContent, NumberContent, ObjectContent, RangeStep, RegexContent, StringContent, Uuid,
@@ -198,7 +198,7 @@ impl SqlxDataSource for PostgresDataSource {
                     RegexContent::pattern(pattern).context("pattern will always compile")?,
                 ))
             }
-            "int2" => Content::Number(NumberContent::I32(I32::Range(RangeStep::default()))),
+            "int2" => Content::Number(NumberContent::I16(I16::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()))),


Test patch:
diff --git a/synth/testing_harness/postgres/0_complete_schema.sql b/synth/testing_harness/postgres/0_complete_schema.sql
--- a/synth/testing_harness/postgres/0_complete_schema.sql
+++ b/synth/testing_harness/postgres/0_complete_schema.sql
@@ -12,7 +12,7 @@ create table types
   string_bpchar bpchar(6) NOT NULL,
   string_name name NOT NULL,
   /* string_uuid uuid NOT NULL, */
-  /* i64_int2 int2 NOT NULL, */
+  i16_int2 int2 NOT NULL, 
   i32_int4 int4 NOT NULL,
   i64_int8 int8 NOT NULL,
   f32_float4 float4 NOT NULL,
diff --git a/synth/testing_harness/postgres/arrays_master/arrays.json b/synth/testing_harness/postgres/arrays_master/arrays.json
--- a/synth/testing_harness/postgres/arrays_master/arrays.json
+++ b/synth/testing_harness/postgres/arrays_master/arrays.json
@@ -135,7 +135,7 @@
           "low": -30856,
           "high": 32726
         },
-        "subtype": "i32"
+        "subtype": "i16"
       }
     },
     "int4_array": {
@@ -463,7 +463,7 @@
           "low": 341,
           "high": 32588
         },
-        "subtype": "i32"
+        "subtype": "i16"
       }
     },
     "uint4_array": {
diff --git a/synth/testing_harness/postgres/arrays_master/unofficial_arrays.json b/synth/testing_harness/postgres/arrays_master/unofficial_arrays.json
--- a/synth/testing_harness/postgres/arrays_master/unofficial_arrays.json
+++ b/synth/testing_harness/postgres/arrays_master/unofficial_arrays.json
@@ -196,7 +196,7 @@
           "low": -31803,
           "high": 31156
         },
-        "subtype": "i32"
+        "subtype": "i16"
       }
     },
     "timestamp_with_time_zone_array": {
@@ -295,8 +295,8 @@
           "low": 531,
           "high": 32717
         },
-        "subtype": "i32"
+        "subtype": "i16"
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/synth/testing_harness/postgres/complete_import_master/types.json b/synth/testing_harness/postgres/complete_import_master/types.json
--- a/synth/testing_harness/postgres/complete_import_master/types.json
+++ b/synth/testing_harness/postgres/complete_import_master/types.json
@@ -86,6 +86,14 @@
         "high": 3.141592653589793
       }
     },
+    "i16_int2": {
+     "type": "number",
+     "subtype": "i16",
+     "range": {
+       "high": -16768,
+       "low": -16768
+     }
+    },
     "date_time_timestamptz": {
       "type": "one_of",
       "variants": [
diff --git a/synth/testing_harness/postgres/complete_import_master/unofficial_types.json b/synth/testing_harness/postgres/complete_import_master/unofficial_types.json
--- a/synth/testing_harness/postgres/complete_import_master/unofficial_types.json
+++ b/synth/testing_harness/postgres/complete_import_master/unofficial_types.json
@@ -80,7 +80,7 @@
     },
     "id_serial2": {
       "type": "number",
-      "subtype": "i32",
+      "subtype": "i16",
       "range": {
         "low": 1,
         "high": 10
@@ -88,7 +88,7 @@
     },
     "id_smallserial": {
       "type": "number",
-      "subtype": "i32",
+      "subtype": "i16",
       "range": {
         "low": 1,
         "high": 10
diff --git a/synth/testing_harness/postgres/complete_master/types.json b/synth/testing_harness/postgres/complete_master/types.json
--- a/synth/testing_harness/postgres/complete_master/types.json
+++ b/synth/testing_harness/postgres/complete_master/types.json
@@ -65,6 +65,11 @@
       "subtype": "f64",
       "constant": 3.14159265358979323
     },
+    "i16_int2": {
+      "type": "number",
+      "subtype": "i16",
+      "constant": -16768
+    },
     "date_time_timestamptz": {
       "type": "date_time",
       "subtype": "date_time",