From 6aa7812a9d3682b523ac27f7494836bfd2c597ee Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Thu, 10 Feb 2022 11:19:12 +0100
Subject: [PATCH 01/20] support ltree

---
 Cargo.toml                            |  1 +
 sqlx-core/Cargo.toml                  |  1 +
 sqlx-core/src/postgres/type_info.rs   | 12 ++++++
 sqlx-core/src/postgres/types/ltree.rs | 61 +++++++++++++++++++++++++++
 sqlx-core/src/postgres/types/mod.rs   |  6 +++
 sqlx-macros/Cargo.toml                |  1 +
 sqlx-macros/src/database/postgres.rs  |  3 ++
 7 files changed, 85 insertions(+)
 create mode 100644 sqlx-core/src/postgres/types/ltree.rs

diff --git a/Cargo.toml b/Cargo.toml
index 7a2ea8ae28..7610f72867 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -130,6 +130,7 @@ time = ["sqlx-core/time", "sqlx-macros/time"]
 bit-vec = ["sqlx-core/bit-vec", "sqlx-macros/bit-vec"]
 bstr = ["sqlx-core/bstr"]
 git2 = ["sqlx-core/git2"]
+ltree = ["sqlx-core/ltree", "sqlx-macros/ltree"]
 
 [dependencies]
 sqlx-core = { version = "0.5.10", path = "sqlx-core", default-features = false }
diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml
index e8912206fa..febffe6da9 100644
--- a/sqlx-core/Cargo.toml
+++ b/sqlx-core/Cargo.toml
@@ -62,6 +62,7 @@ all-types = [
 bigdecimal = ["bigdecimal_", "num-bigint"]
 decimal = ["rust_decimal", "num-bigint"]
 json = ["serde", "serde_json"]
+ltree = ["postgres"]
 
 # runtimes
 runtime-actix-native-tls = [
diff --git a/sqlx-core/src/postgres/type_info.rs b/sqlx-core/src/postgres/type_info.rs
index 97d5efa0cc..d578a8af94 100644
--- a/sqlx-core/src/postgres/type_info.rs
+++ b/sqlx-core/src/postgres/type_info.rs
@@ -116,6 +116,7 @@ pub enum PgType {
     JsonpathArray,
     Money,
     MoneyArray,
+    Ltree,
 
     // https://www.postgresql.org/docs/9.3/datatype-pseudo.html
     Void,
@@ -328,6 +329,7 @@ impl PgType {
             3927 => PgType::Int8RangeArray,
             4072 => PgType::Jsonpath,
             4073 => PgType::JsonpathArray,
+            16394 => PgType::Ltree,
 
             _ => {
                 return None;
@@ -436,6 +438,7 @@ impl PgType {
             PgType::Int8RangeArray => 3927,
             PgType::Jsonpath => 4072,
             PgType::JsonpathArray => 4073,
+            PgType::Ltree => 16394,
             PgType::Custom(ty) => ty.oid,
 
             PgType::DeclareWithOid(oid) => *oid,
@@ -538,6 +541,7 @@ impl PgType {
             PgType::JsonpathArray => "JSONPATH[]",
             PgType::Money => "MONEY",
             PgType::MoneyArray => "MONEY[]",
+            PgType::Ltree => "LTREE",
             PgType::Void => "VOID",
             PgType::Custom(ty) => &*ty.name,
             PgType::DeclareWithOid(_) => "?",
@@ -638,6 +642,7 @@ impl PgType {
             PgType::JsonpathArray => "_jsonpath",
             PgType::Money => "money",
             PgType::MoneyArray => "_money",
+            PgType::Ltree => "ltree",
             PgType::Void => "void",
             PgType::Custom(ty) => &*ty.name,
             PgType::DeclareWithOid(_) => "?",
@@ -738,6 +743,7 @@ impl PgType {
             PgType::JsonpathArray => &PgTypeKind::Array(PgTypeInfo(PgType::Jsonpath)),
             PgType::Money => &PgTypeKind::Simple,
             PgType::MoneyArray => &PgTypeKind::Array(PgTypeInfo(PgType::Money)),
+            PgType::Ltree => &PgTypeKind::Simple,
 
             PgType::Void => &PgTypeKind::Pseudo,
 
@@ -849,6 +855,7 @@ impl PgType {
             PgType::Int8RangeArray => Some(Cow::Owned(PgTypeInfo(PgType::Int8Range))),
             PgType::Jsonpath => None,
             PgType::JsonpathArray => Some(Cow::Owned(PgTypeInfo(PgType::Jsonpath))),
+            PgType::Ltree => None,
             // There is no `UnknownArray`
             PgType::Unknown => None,
             // There is no `VoidArray`
@@ -1101,6 +1108,11 @@ impl PgTypeInfo {
     pub(crate) const INT8_RANGE: Self = Self(PgType::Int8Range);
     pub(crate) const INT8_RANGE_ARRAY: Self = Self(PgType::Int8RangeArray);
 
+    //
+    // ltree
+
+    pub(crate) const LTREE: Self = Self(PgType::Ltree);
+
     //
     // pseudo types
     // https://www.postgresql.org/docs/9.3/datatype-pseudo.html
diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
new file mode 100644
index 0000000000..9f8f7f4b97
--- /dev/null
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -0,0 +1,61 @@
+use std::fmt::{self, Display, Formatter};
+use std::str::FromStr;
+use crate::decode::Decode;
+use crate::encode::{Encode, IsNull};
+use crate::error::{Error, BoxDynError};
+use crate::postgres::{PgArgumentBuffer, PgValueFormat, PgTypeInfo, PgValueRef, Postgres};
+use crate::types::Type;
+
+#[derive(Debug)]
+pub struct PgLTree {
+    labels: Vec<String>
+}
+
+impl FromStr for PgLTree {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        Ok(
+            Self {
+                labels: s.split('.').map(|s| s.to_owned()).collect()
+            }
+        )
+    }
+}
+
+impl Display for PgLTree {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.labels.join("."))
+    }
+}
+
+impl Type<Postgres> for PgLTree {
+    fn type_info() -> PgTypeInfo {
+        PgTypeInfo::LTREE
+    }
+}
+
+impl Encode<'_, Postgres> for PgLTree {
+    fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull {
+        buf.extend(1i8.to_le_bytes());
+        buf.extend(self.to_string().as_bytes());
+
+        IsNull::No
+    }
+}
+
+impl<'r> Decode<'r, Postgres> for PgLTree {
+    fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
+        match value.format() {
+            PgValueFormat::Binary => {
+                let bytes = value.as_bytes()?;
+                let version = i8::from_le_bytes([bytes[0]; 1]);
+                if version != 1 {
+                    todo!("add error here")
+                }
+                Ok(Self::from_str(&String::from_utf8(bytes[1..].to_vec())?)?)
+            },
+            PgValueFormat::Text => Ok(Self::from_str(value.as_str()?)?)
+        }
+    }
+}
diff --git a/sqlx-core/src/postgres/types/mod.rs b/sqlx-core/src/postgres/types/mod.rs
index eaf11f1093..2aebcdd06a 100644
--- a/sqlx-core/src/postgres/types/mod.rs
+++ b/sqlx-core/src/postgres/types/mod.rs
@@ -208,6 +208,12 @@ mod mac_address;
 #[cfg(feature = "bit-vec")]
 mod bit_vec;
 
+#[cfg(feature = "ltree")]
+mod ltree;
+
+#[cfg(feature = "ltree")]
+pub use ltree::PgLTree;
+
 pub use array::PgHasArrayType;
 pub use interval::PgInterval;
 pub use money::PgMoney;
diff --git a/sqlx-macros/Cargo.toml b/sqlx-macros/Cargo.toml
index b036aa2f46..52f849cb5a 100644
--- a/sqlx-macros/Cargo.toml
+++ b/sqlx-macros/Cargo.toml
@@ -76,6 +76,7 @@ mac_address = ["sqlx-core/mac_address"]
 uuid = ["sqlx-core/uuid"]
 bit-vec = ["sqlx-core/bit-vec"]
 json = ["sqlx-core/json", "serde_json"]
+ltree = ["sqlx-core/ltree"]
 
 [dependencies]
 dotenv = { version = "0.15.0", default-features = false }
diff --git a/sqlx-macros/src/database/postgres.rs b/sqlx-macros/src/database/postgres.rs
index 5330bb3cd9..e3a554ecaf 100644
--- a/sqlx-macros/src/database/postgres.rs
+++ b/sqlx-macros/src/database/postgres.rs
@@ -69,6 +69,9 @@ impl_database_ext! {
         #[cfg(feature = "bit-vec")]
         sqlx::types::BitVec,
 
+        #[cfg(feature = "ltree")]
+        sqlx::postgres::types::PgLTree,
+
         // Arrays
 
         Vec<bool> | &[bool],

From 81ba1b80f52c80f16c8e364a4fd95cc86921b54d Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Thu, 10 Feb 2022 13:07:42 +0100
Subject: [PATCH 02/20] add default and push to PgLTree

---
 sqlx-core/src/error.rs                |  5 +++++
 sqlx-core/src/postgres/types/ltree.rs | 13 ++++++++++++-
 2 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs
index 0659375846..4761fbb2c9 100644
--- a/sqlx-core/src/error.rs
+++ b/sqlx-core/src/error.rs
@@ -103,6 +103,11 @@ pub enum Error {
     #[cfg(feature = "migrate")]
     #[error("{0}")]
     Migrate(#[source] Box<crate::migrate::MigrateError>),
+
+    /// A background worker has crashed.
+    #[cfg(feature = "ltree")]
+    #[error("ltree label cotains invalid characters")]
+    InvalidLtreeLabel,
 }
 
 impl StdError for Box<dyn DatabaseError> {}
diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index 9f8f7f4b97..aaf5962151 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -6,11 +6,22 @@ use crate::error::{Error, BoxDynError};
 use crate::postgres::{PgArgumentBuffer, PgValueFormat, PgTypeInfo, PgValueRef, Postgres};
 use crate::types::Type;
 
-#[derive(Debug)]
+#[derive(Debug, Default)]
 pub struct PgLTree {
     labels: Vec<String>
 }
 
+impl PgLTree {
+    pub fn push(&mut self, label: String) -> Result<(), Error> {
+        if label.chars().all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit() || c == '_') {
+            self.labels.push(label);
+            Ok(())
+        } else {
+            Err(Error::InvalidLtreeLabel)
+        }
+    }
+}
+
 impl FromStr for PgLTree {
     type Err = Error;
 

From 0c8a5a8c4c2fb940304d0adb47e117fde3761470 Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Thu, 10 Feb 2022 13:36:40 +0100
Subject: [PATCH 03/20] add more derived for ltree

---
 sqlx-core/src/postgres/types/ltree.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index aaf5962151..0f2bb4913e 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -6,7 +6,7 @@ use crate::error::{Error, BoxDynError};
 use crate::postgres::{PgArgumentBuffer, PgValueFormat, PgTypeInfo, PgValueRef, Postgres};
 use crate::types::Type;
 
-#[derive(Debug, Default)]
+#[derive(Clone, Debug, Default, PartialEq)]
 pub struct PgLTree {
     labels: Vec<String>
 }

From 5e5dd75375da9429f6e7b69ae569548d24f4ebae Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Thu, 10 Feb 2022 13:53:28 +0100
Subject: [PATCH 04/20] fix copy/paste

---
 sqlx-core/src/error.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs
index 4761fbb2c9..8aa47962ca 100644
--- a/sqlx-core/src/error.rs
+++ b/sqlx-core/src/error.rs
@@ -104,7 +104,7 @@ pub enum Error {
     #[error("{0}")]
     Migrate(#[source] Box<crate::migrate::MigrateError>),
 
-    /// A background worker has crashed.
+    /// LTree labels can only contain [A-Za-z_]
     #[cfg(feature = "ltree")]
     #[error("ltree label cotains invalid characters")]
     InvalidLtreeLabel,

From 98c99012f8beb0714798679b638d604c16508253 Mon Sep 17 00:00:00 2001
From: Bastian <b.schubert82@gmail.com>
Date: Thu, 10 Feb 2022 17:09:29 +0100
Subject: [PATCH 05/20] Update sqlx-core/src/error.rs

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
---
 sqlx-core/src/error.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs
index 8aa47962ca..00333015b4 100644
--- a/sqlx-core/src/error.rs
+++ b/sqlx-core/src/error.rs
@@ -104,7 +104,7 @@ pub enum Error {
     #[error("{0}")]
     Migrate(#[source] Box<crate::migrate::MigrateError>),
 
-    /// LTree labels can only contain [A-Za-z_]
+    /// LTree labels can only contain [A-Za-z0-9_]
     #[cfg(feature = "ltree")]
     #[error("ltree label cotains invalid characters")]
     InvalidLtreeLabel,

From b5ff590596c1678cafd051ed5a79fdeb7096e94d Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Thu, 10 Feb 2022 19:49:45 +0100
Subject: [PATCH 06/20] PR fixes

---
 sqlx-core/src/error.rs                |  5 ++++
 sqlx-core/src/postgres/types/ltree.rs | 40 ++++++++++++++++-----------
 2 files changed, 29 insertions(+), 16 deletions(-)

diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs
index 00333015b4..528ad5ae04 100644
--- a/sqlx-core/src/error.rs
+++ b/sqlx-core/src/error.rs
@@ -108,6 +108,11 @@ pub enum Error {
     #[cfg(feature = "ltree")]
     #[error("ltree label cotains invalid characters")]
     InvalidLtreeLabel,
+
+    /// LTree version not supported
+    #[cfg(feature = "ltree")]
+    #[error("ltree version not supported")]
+    InvalidLtreeVersion,
 }
 
 impl StdError for Box<dyn DatabaseError> {}
diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index 0f2bb4913e..2cf15a1927 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -1,19 +1,22 @@
-use std::fmt::{self, Display, Formatter};
-use std::str::FromStr;
 use crate::decode::Decode;
 use crate::encode::{Encode, IsNull};
-use crate::error::{Error, BoxDynError};
-use crate::postgres::{PgArgumentBuffer, PgValueFormat, PgTypeInfo, PgValueRef, Postgres};
+use crate::error::{BoxDynError, Error};
+use crate::postgres::{PgArgumentBuffer, PgTypeInfo, PgValueFormat, PgValueRef, Postgres};
 use crate::types::Type;
+use std::fmt::{self, Display, Formatter};
+use std::str::FromStr;
 
 #[derive(Clone, Debug, Default, PartialEq)]
 pub struct PgLTree {
-    labels: Vec<String>
+    labels: Vec<String>,
 }
 
 impl PgLTree {
     pub fn push(&mut self, label: String) -> Result<(), Error> {
-        if label.chars().all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit() || c == '_') {
+        if label
+            .chars()
+            .all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit() || c == '_')
+        {
             self.labels.push(label);
             Ok(())
         } else {
@@ -26,17 +29,22 @@ impl FromStr for PgLTree {
     type Err = Error;
 
     fn from_str(s: &str) -> Result<Self, Error> {
-        Ok(
-            Self {
-                labels: s.split('.').map(|s| s.to_owned()).collect()
-            }
-        )
+        Ok(Self {
+            labels: s.split('.').map(|s| s.to_owned()).collect(),
+        })
     }
 }
 
 impl Display for PgLTree {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", self.labels.join("."))
+        let mut iter = self.labels.iter();
+        if let Some(label) = iter.next() {
+            write!(f, "{}", label)?;
+            while let Some(label) = iter.next() {
+                write!(f, ".{}", label)?;
+            }
+        }
+        Ok(())
     }
 }
 
@@ -62,11 +70,11 @@ impl<'r> Decode<'r, Postgres> for PgLTree {
                 let bytes = value.as_bytes()?;
                 let version = i8::from_le_bytes([bytes[0]; 1]);
                 if version != 1 {
-                    todo!("add error here")
+                    return Err(Box::new(Error::InvalidLtreeVersion));
                 }
-                Ok(Self::from_str(&String::from_utf8(bytes[1..].to_vec())?)?)
-            },
-            PgValueFormat::Text => Ok(Self::from_str(value.as_str()?)?)
+                Ok(Self::from_str(std::str::from_utf8(&bytes[1..])?)?)
+            }
+            PgValueFormat::Text => Ok(Self::from_str(value.as_str()?)?),
         }
     }
 }

From 95bf07b6f612fa6357b4e6d3bbd61eb17e60d345 Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Fri, 11 Feb 2022 08:55:33 +0100
Subject: [PATCH 07/20] ltree with name instead of OID

---
 sqlx-core/src/postgres/type_info.rs   | 12 ------------
 sqlx-core/src/postgres/types/ltree.rs |  2 +-
 2 files changed, 1 insertion(+), 13 deletions(-)

diff --git a/sqlx-core/src/postgres/type_info.rs b/sqlx-core/src/postgres/type_info.rs
index d578a8af94..97d5efa0cc 100644
--- a/sqlx-core/src/postgres/type_info.rs
+++ b/sqlx-core/src/postgres/type_info.rs
@@ -116,7 +116,6 @@ pub enum PgType {
     JsonpathArray,
     Money,
     MoneyArray,
-    Ltree,
 
     // https://www.postgresql.org/docs/9.3/datatype-pseudo.html
     Void,
@@ -329,7 +328,6 @@ impl PgType {
             3927 => PgType::Int8RangeArray,
             4072 => PgType::Jsonpath,
             4073 => PgType::JsonpathArray,
-            16394 => PgType::Ltree,
 
             _ => {
                 return None;
@@ -438,7 +436,6 @@ impl PgType {
             PgType::Int8RangeArray => 3927,
             PgType::Jsonpath => 4072,
             PgType::JsonpathArray => 4073,
-            PgType::Ltree => 16394,
             PgType::Custom(ty) => ty.oid,
 
             PgType::DeclareWithOid(oid) => *oid,
@@ -541,7 +538,6 @@ impl PgType {
             PgType::JsonpathArray => "JSONPATH[]",
             PgType::Money => "MONEY",
             PgType::MoneyArray => "MONEY[]",
-            PgType::Ltree => "LTREE",
             PgType::Void => "VOID",
             PgType::Custom(ty) => &*ty.name,
             PgType::DeclareWithOid(_) => "?",
@@ -642,7 +638,6 @@ impl PgType {
             PgType::JsonpathArray => "_jsonpath",
             PgType::Money => "money",
             PgType::MoneyArray => "_money",
-            PgType::Ltree => "ltree",
             PgType::Void => "void",
             PgType::Custom(ty) => &*ty.name,
             PgType::DeclareWithOid(_) => "?",
@@ -743,7 +738,6 @@ impl PgType {
             PgType::JsonpathArray => &PgTypeKind::Array(PgTypeInfo(PgType::Jsonpath)),
             PgType::Money => &PgTypeKind::Simple,
             PgType::MoneyArray => &PgTypeKind::Array(PgTypeInfo(PgType::Money)),
-            PgType::Ltree => &PgTypeKind::Simple,
 
             PgType::Void => &PgTypeKind::Pseudo,
 
@@ -855,7 +849,6 @@ impl PgType {
             PgType::Int8RangeArray => Some(Cow::Owned(PgTypeInfo(PgType::Int8Range))),
             PgType::Jsonpath => None,
             PgType::JsonpathArray => Some(Cow::Owned(PgTypeInfo(PgType::Jsonpath))),
-            PgType::Ltree => None,
             // There is no `UnknownArray`
             PgType::Unknown => None,
             // There is no `VoidArray`
@@ -1108,11 +1101,6 @@ impl PgTypeInfo {
     pub(crate) const INT8_RANGE: Self = Self(PgType::Int8Range);
     pub(crate) const INT8_RANGE_ARRAY: Self = Self(PgType::Int8RangeArray);
 
-    //
-    // ltree
-
-    pub(crate) const LTREE: Self = Self(PgType::Ltree);
-
     //
     // pseudo types
     // https://www.postgresql.org/docs/9.3/datatype-pseudo.html
diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index 2cf15a1927..5d086e0d34 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -50,7 +50,7 @@ impl Display for PgLTree {
 
 impl Type<Postgres> for PgLTree {
     fn type_info() -> PgTypeInfo {
-        PgTypeInfo::LTREE
+        PgTypeInfo::with_name("ltree")
     }
 }
 

From 4f19c9b816e34c7dc46be03117a1caf495cd6467 Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Fri, 11 Feb 2022 09:02:49 +0100
Subject: [PATCH 08/20] custom ltree errors

---
 sqlx-core/src/error.rs                | 10 ----------
 sqlx-core/src/postgres/types/ltree.rs | 19 ++++++++++++++++++-
 2 files changed, 18 insertions(+), 11 deletions(-)

diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs
index 528ad5ae04..0659375846 100644
--- a/sqlx-core/src/error.rs
+++ b/sqlx-core/src/error.rs
@@ -103,16 +103,6 @@ pub enum Error {
     #[cfg(feature = "migrate")]
     #[error("{0}")]
     Migrate(#[source] Box<crate::migrate::MigrateError>),
-
-    /// LTree labels can only contain [A-Za-z0-9_]
-    #[cfg(feature = "ltree")]
-    #[error("ltree label cotains invalid characters")]
-    InvalidLtreeLabel,
-
-    /// LTree version not supported
-    #[cfg(feature = "ltree")]
-    #[error("ltree version not supported")]
-    InvalidLtreeVersion,
 }
 
 impl StdError for Box<dyn DatabaseError> {}
diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index 5d086e0d34..b344b41d84 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -1,11 +1,28 @@
 use crate::decode::Decode;
 use crate::encode::{Encode, IsNull};
-use crate::error::{BoxDynError, Error};
+use crate::error::BoxDynError;
 use crate::postgres::{PgArgumentBuffer, PgTypeInfo, PgValueFormat, PgValueRef, Postgres};
 use crate::types::Type;
 use std::fmt::{self, Display, Formatter};
 use std::str::FromStr;
 
+
+/// Represents ltree specific errors
+#[derive(Debug, thiserror::Error)]
+#[non_exhaustive]
+pub enum Error {
+   /// LTree labels can only contain [A-Za-z0-9_]
+    #[cfg(feature = "ltree")]
+    #[error("ltree label cotains invalid characters")]
+    InvalidLtreeLabel,
+
+    /// LTree version not supported
+    #[cfg(feature = "ltree")]
+    #[error("ltree version not supported")]
+    InvalidLtreeVersion,
+}
+
+
 #[derive(Clone, Debug, Default, PartialEq)]
 pub struct PgLTree {
     labels: Vec<String>,

From b611e5f75cd8326db7bf99ae28dce97d95f65fd5 Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Fri, 11 Feb 2022 09:15:48 +0100
Subject: [PATCH 09/20] add pop ot PgLTree

---
 sqlx-core/src/postgres/types/ltree.rs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index b344b41d84..683482074d 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -40,6 +40,10 @@ impl PgLTree {
             Err(Error::InvalidLtreeLabel)
         }
     }
+
+    pub fn pop(&mut self) -> Option<String> {
+        self.labels.pop()
+    }
 }
 
 impl FromStr for PgLTree {

From b9bced314b85bcad1bdc0670fd9a8ed80dddd0c5 Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Fri, 11 Feb 2022 09:25:31 +0100
Subject: [PATCH 10/20] do not hide ltree behind feature flag

---
 Cargo.toml                            | 1 -
 sqlx-core/Cargo.toml                  | 1 -
 sqlx-core/src/postgres/types/ltree.rs | 3 +--
 sqlx-core/src/postgres/types/mod.rs   | 6 +-----
 sqlx-macros/Cargo.toml                | 1 -
 sqlx-macros/src/database/postgres.rs  | 5 ++---
 6 files changed, 4 insertions(+), 13 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 7610f72867..7a2ea8ae28 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -130,7 +130,6 @@ time = ["sqlx-core/time", "sqlx-macros/time"]
 bit-vec = ["sqlx-core/bit-vec", "sqlx-macros/bit-vec"]
 bstr = ["sqlx-core/bstr"]
 git2 = ["sqlx-core/git2"]
-ltree = ["sqlx-core/ltree", "sqlx-macros/ltree"]
 
 [dependencies]
 sqlx-core = { version = "0.5.10", path = "sqlx-core", default-features = false }
diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml
index febffe6da9..e8912206fa 100644
--- a/sqlx-core/Cargo.toml
+++ b/sqlx-core/Cargo.toml
@@ -62,7 +62,6 @@ all-types = [
 bigdecimal = ["bigdecimal_", "num-bigint"]
 decimal = ["rust_decimal", "num-bigint"]
 json = ["serde", "serde_json"]
-ltree = ["postgres"]
 
 # runtimes
 runtime-actix-native-tls = [
diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index 683482074d..37c6b4dd5f 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -12,17 +12,16 @@ use std::str::FromStr;
 #[non_exhaustive]
 pub enum Error {
    /// LTree labels can only contain [A-Za-z0-9_]
-    #[cfg(feature = "ltree")]
     #[error("ltree label cotains invalid characters")]
     InvalidLtreeLabel,
 
     /// LTree version not supported
-    #[cfg(feature = "ltree")]
     #[error("ltree version not supported")]
     InvalidLtreeVersion,
 }
 
 
+/// Represents an postgres ltree. Not that this is an EXTENSION!
 #[derive(Clone, Debug, Default, PartialEq)]
 pub struct PgLTree {
     labels: Vec<String>,
diff --git a/sqlx-core/src/postgres/types/mod.rs b/sqlx-core/src/postgres/types/mod.rs
index 2aebcdd06a..8c40360c78 100644
--- a/sqlx-core/src/postgres/types/mod.rs
+++ b/sqlx-core/src/postgres/types/mod.rs
@@ -174,6 +174,7 @@ mod record;
 mod str;
 mod tuple;
 mod void;
+mod ltree;
 
 #[cfg(any(feature = "chrono", feature = "time"))]
 mod time_tz;
@@ -208,12 +209,7 @@ mod mac_address;
 #[cfg(feature = "bit-vec")]
 mod bit_vec;
 
-#[cfg(feature = "ltree")]
-mod ltree;
-
-#[cfg(feature = "ltree")]
 pub use ltree::PgLTree;
-
 pub use array::PgHasArrayType;
 pub use interval::PgInterval;
 pub use money::PgMoney;
diff --git a/sqlx-macros/Cargo.toml b/sqlx-macros/Cargo.toml
index 52f849cb5a..b036aa2f46 100644
--- a/sqlx-macros/Cargo.toml
+++ b/sqlx-macros/Cargo.toml
@@ -76,7 +76,6 @@ mac_address = ["sqlx-core/mac_address"]
 uuid = ["sqlx-core/uuid"]
 bit-vec = ["sqlx-core/bit-vec"]
 json = ["sqlx-core/json", "serde_json"]
-ltree = ["sqlx-core/ltree"]
 
 [dependencies]
 dotenv = { version = "0.15.0", default-features = false }
diff --git a/sqlx-macros/src/database/postgres.rs b/sqlx-macros/src/database/postgres.rs
index e3a554ecaf..3d51641d34 100644
--- a/sqlx-macros/src/database/postgres.rs
+++ b/sqlx-macros/src/database/postgres.rs
@@ -18,6 +18,8 @@ impl_database_ext! {
 
         sqlx::postgres::types::PgMoney,
 
+        sqlx::postgres::types::PgLTree,
+
         #[cfg(feature = "uuid")]
         sqlx::types::Uuid,
 
@@ -69,9 +71,6 @@ impl_database_ext! {
         #[cfg(feature = "bit-vec")]
         sqlx::types::BitVec,
 
-        #[cfg(feature = "ltree")]
-        sqlx::postgres::types::PgLTree,
-
         // Arrays
 
         Vec<bool> | &[bool],

From 6bb8b26e58bc34e7c98dbcd73f742707907e33da Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Fri, 11 Feb 2022 10:26:25 +0100
Subject: [PATCH 11/20] bytes() instead of chars()

---
 sqlx-core/src/postgres/types/ltree.rs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index 37c6b4dd5f..f800cf6d7a 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -30,8 +30,8 @@ pub struct PgLTree {
 impl PgLTree {
     pub fn push(&mut self, label: String) -> Result<(), Error> {
         if label
-            .chars()
-            .all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit() || c == '_')
+            .bytes()
+            .all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit() || c == b'_')
         {
             self.labels.push(label);
             Ok(())

From e12110ec4b289a0e17102973deca418fc7222d0a Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Fri, 11 Feb 2022 10:31:22 +0100
Subject: [PATCH 12/20] apply extend_display suggestion

---
 sqlx-core/src/postgres/arguments.rs   | 19 +++++++++++++++++++
 sqlx-core/src/postgres/types/ltree.rs |  2 +-
 2 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/sqlx-core/src/postgres/arguments.rs b/sqlx-core/src/postgres/arguments.rs
index 9bd60dbbbd..83a22a99e4 100644
--- a/sqlx-core/src/postgres/arguments.rs
+++ b/sqlx-core/src/postgres/arguments.rs
@@ -1,4 +1,5 @@
 use std::ops::{Deref, DerefMut};
+use std::fmt::{self, Write};
 
 use crate::arguments::Arguments;
 use crate::encode::{Encode, IsNull};
@@ -161,6 +162,24 @@ impl PgArgumentBuffer {
         self.extend_from_slice(&0_u32.to_be_bytes());
         self.type_holes.push((offset, type_name.clone()));
     }
+
+    pub(crate) fn extend_display<T: fmt::Display>(&mut self, value: T) {
+        struct VecFmt<'a>(&'a mut Vec<u8>);
+
+        impl<'a> fmt::Write for VecFmt<'a> {
+            fn write_char(&mut self, c: char) -> fmt::Result {
+                self.write_str(c.encode_utf8(&mut [0u8; 4]))
+            }
+
+            fn write_str(&mut self, s: &str) -> fmt::Result {
+                self.0.extend_from_slice(s.as_bytes());
+                Ok(())
+            }
+        }
+
+        write!(VecFmt(&mut self.buffer), "{}", value)
+            .expect("Display implementation panicked while writing to PgArgumentBuffer");
+    }
 }
 
 impl Deref for PgArgumentBuffer {
diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index f800cf6d7a..5c0b643972 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -77,7 +77,7 @@ impl Type<Postgres> for PgLTree {
 impl Encode<'_, Postgres> for PgLTree {
     fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull {
         buf.extend(1i8.to_le_bytes());
-        buf.extend(self.to_string().as_bytes());
+        buf.extend_display(self);
 
         IsNull::No
     }

From 1bb6d9928e4c02b6c23e9dd500f5ded8938b283f Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Fri, 11 Feb 2022 10:33:44 +0100
Subject: [PATCH 13/20] add more functions to PgLTree

---
 sqlx-core/src/postgres/arguments.rs   |  2 +-
 sqlx-core/src/postgres/types/ltree.rs | 37 ++++++++++++++++++++++++---
 sqlx-core/src/postgres/types/mod.rs   |  4 +--
 3 files changed, 37 insertions(+), 6 deletions(-)

diff --git a/sqlx-core/src/postgres/arguments.rs b/sqlx-core/src/postgres/arguments.rs
index 83a22a99e4..b8ba8bbf42 100644
--- a/sqlx-core/src/postgres/arguments.rs
+++ b/sqlx-core/src/postgres/arguments.rs
@@ -1,5 +1,5 @@
-use std::ops::{Deref, DerefMut};
 use std::fmt::{self, Write};
+use std::ops::{Deref, DerefMut};
 
 use crate::arguments::Arguments;
 use crate::encode::{Encode, IsNull};
diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index 5c0b643972..f8fad397c3 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -4,14 +4,14 @@ use crate::error::BoxDynError;
 use crate::postgres::{PgArgumentBuffer, PgTypeInfo, PgValueFormat, PgValueRef, Postgres};
 use crate::types::Type;
 use std::fmt::{self, Display, Formatter};
+use std::iter::FromIterator;
 use std::str::FromStr;
 
-
 /// Represents ltree specific errors
 #[derive(Debug, thiserror::Error)]
 #[non_exhaustive]
 pub enum Error {
-   /// LTree labels can only contain [A-Za-z0-9_]
+    /// LTree labels can only contain [A-Za-z0-9_]
     #[error("ltree label cotains invalid characters")]
     InvalidLtreeLabel,
 
@@ -20,7 +20,6 @@ pub enum Error {
     InvalidLtreeVersion,
 }
 
-
 /// Represents an postgres ltree. Not that this is an EXTENSION!
 #[derive(Clone, Debug, Default, PartialEq)]
 pub struct PgLTree {
@@ -28,6 +27,28 @@ pub struct PgLTree {
 }
 
 impl PgLTree {
+    /// creates default/empty ltree
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// creates ltree from a [Vec<String>] without checking labels
+    pub fn new_unchecked(labels: Vec<String>) -> Self {
+        Self { labels }
+    }
+
+    /// creates ltree from an iterator with checking labels
+    pub fn from_iter(
+        labels: impl IntoIterator<Item = String, IntoIter = std::vec::IntoIter<String>>,
+    ) -> Result<Self, Error> {
+        let mut ltree = Self::default();
+        for label in labels {
+            ltree.push(label)?;
+        }
+        Ok(ltree)
+    }
+
+    /// push a label to ltree
     pub fn push(&mut self, label: String) -> Result<(), Error> {
         if label
             .bytes()
@@ -40,11 +61,21 @@ impl PgLTree {
         }
     }
 
+    /// pop a label from ltree
     pub fn pop(&mut self) -> Option<String> {
         self.labels.pop()
     }
 }
 
+impl IntoIterator for PgLTree {
+    type Item = String;
+    type IntoIter = std::vec::IntoIter<Self::Item>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.labels.into_iter()
+    }
+}
+
 impl FromStr for PgLTree {
     type Err = Error;
 
diff --git a/sqlx-core/src/postgres/types/mod.rs b/sqlx-core/src/postgres/types/mod.rs
index 8c40360c78..c67658e043 100644
--- a/sqlx-core/src/postgres/types/mod.rs
+++ b/sqlx-core/src/postgres/types/mod.rs
@@ -168,13 +168,13 @@ mod bytes;
 mod float;
 mod int;
 mod interval;
+mod ltree;
 mod money;
 mod range;
 mod record;
 mod str;
 mod tuple;
 mod void;
-mod ltree;
 
 #[cfg(any(feature = "chrono", feature = "time"))]
 mod time_tz;
@@ -209,9 +209,9 @@ mod mac_address;
 #[cfg(feature = "bit-vec")]
 mod bit_vec;
 
-pub use ltree::PgLTree;
 pub use array::PgHasArrayType;
 pub use interval::PgInterval;
+pub use ltree::PgLTree;
 pub use money::PgMoney;
 pub use range::PgRange;
 

From cd29037072ef46c04a0c1308d58f5b7c5905a536 Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Fri, 11 Feb 2022 11:13:58 +0100
Subject: [PATCH 14/20] fix IntoIter

---
 sqlx-core/src/postgres/types/ltree.rs | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index f8fad397c3..6510a922e1 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -4,7 +4,6 @@ use crate::error::BoxDynError;
 use crate::postgres::{PgArgumentBuffer, PgTypeInfo, PgValueFormat, PgValueRef, Postgres};
 use crate::types::Type;
 use std::fmt::{self, Display, Formatter};
-use std::iter::FromIterator;
 use std::str::FromStr;
 
 /// Represents ltree specific errors
@@ -39,10 +38,10 @@ impl PgLTree {
 
     /// creates ltree from an iterator with checking labels
     pub fn from_iter(
-        labels: impl IntoIterator<Item = String, IntoIter = std::vec::IntoIter<String>>,
+        labels: impl IntoIterator<Item = String>,
     ) -> Result<Self, Error> {
         let mut ltree = Self::default();
-        for label in labels {
+        for label in labels.into_iter() {
             ltree.push(label)?;
         }
         Ok(ltree)

From e83d38016ebd1e2452c0cf5e44ee07e921ee0383 Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Sat, 12 Feb 2022 07:19:54 +0100
Subject: [PATCH 15/20] resolve PR annotation

---
 sqlx-core/src/postgres/arguments.rs   |  5 ++-
 sqlx-core/src/postgres/types/ltree.rs | 54 +++++++++++++++++++--------
 sqlx-core/src/postgres/types/mod.rs   |  1 +
 3 files changed, 42 insertions(+), 18 deletions(-)

diff --git a/sqlx-core/src/postgres/arguments.rs b/sqlx-core/src/postgres/arguments.rs
index b8ba8bbf42..c94d4c45fe 100644
--- a/sqlx-core/src/postgres/arguments.rs
+++ b/sqlx-core/src/postgres/arguments.rs
@@ -1,4 +1,5 @@
-use std::fmt::{self, Write};
+use std::fmt;
+use std::io::Write;
 use std::ops::{Deref, DerefMut};
 
 use crate::arguments::Arguments;
@@ -177,7 +178,7 @@ impl PgArgumentBuffer {
             }
         }
 
-        write!(VecFmt(&mut self.buffer), "{}", value)
+        write!(self.buffer, "{}", value)
             .expect("Display implementation panicked while writing to PgArgumentBuffer");
     }
 }
diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index 6510a922e1..6e2a512b0a 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -4,12 +4,13 @@ use crate::error::BoxDynError;
 use crate::postgres::{PgArgumentBuffer, PgTypeInfo, PgValueFormat, PgValueRef, Postgres};
 use crate::types::Type;
 use std::fmt::{self, Display, Formatter};
+use std::ops::Deref;
 use std::str::FromStr;
 
 /// Represents ltree specific errors
 #[derive(Debug, thiserror::Error)]
 #[non_exhaustive]
-pub enum Error {
+pub enum PgLTreeParseError {
     /// LTree labels can only contain [A-Za-z0-9_]
     #[error("ltree label cotains invalid characters")]
     InvalidLtreeLabel,
@@ -19,7 +20,16 @@ pub enum Error {
     InvalidLtreeVersion,
 }
 
-/// Represents an postgres ltree. Not that this is an EXTENSION!
+/// Container for a Label Tree (`ltree`) in Postgres.
+///
+/// See https://www.postgresql.org/docs/current/ltree.html
+///
+/// ### Note: Extension Required
+/// The `ltree` extension is not enabled by default in Postgres. You will need to do so explicitly:
+///
+/// ```ignore
+/// CREATE EXTENSION IF NOT EXISTS "ltree";
+/// ```
 #[derive(Clone, Debug, Default, PartialEq)]
 pub struct PgLTree {
     labels: Vec<String>,
@@ -37,26 +47,29 @@ impl PgLTree {
     }
 
     /// creates ltree from an iterator with checking labels
-    pub fn from_iter(
-        labels: impl IntoIterator<Item = String>,
-    ) -> Result<Self, Error> {
+    pub fn from_iter<I, S>(labels: I) -> Result<Self, PgLTreeParseError>
+    where
+        S: Into<String>,
+        I: IntoIterator<Item = S>,
+    {
         let mut ltree = Self::default();
-        for label in labels.into_iter() {
-            ltree.push(label)?;
+        for label in labels {
+            ltree.push(label.into())?;
         }
         Ok(ltree)
     }
 
     /// push a label to ltree
-    pub fn push(&mut self, label: String) -> Result<(), Error> {
-        if label
-            .bytes()
-            .all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit() || c == b'_')
+    pub fn push(&mut self, label: String) -> Result<(), PgLTreeParseError> {
+        if label.len() <= 256
+            && label
+                .bytes()
+                .all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit() || c == b'_')
         {
             self.labels.push(label);
             Ok(())
         } else {
-            Err(Error::InvalidLtreeLabel)
+            Err(PgLTreeParseError::InvalidLtreeLabel)
         }
     }
 
@@ -76,9 +89,9 @@ impl IntoIterator for PgLTree {
 }
 
 impl FromStr for PgLTree {
-    type Err = Error;
+    type Err = PgLTreeParseError;
 
-    fn from_str(s: &str) -> Result<Self, Error> {
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
         Ok(Self {
             labels: s.split('.').map(|s| s.to_owned()).collect(),
         })
@@ -90,7 +103,7 @@ impl Display for PgLTree {
         let mut iter = self.labels.iter();
         if let Some(label) = iter.next() {
             write!(f, "{}", label)?;
-            while let Some(label) = iter.next() {
+            for label in iter {
                 write!(f, ".{}", label)?;
             }
         }
@@ -98,8 +111,17 @@ impl Display for PgLTree {
     }
 }
 
+impl Deref for PgLTree {
+    type Target = [String];
+
+    fn deref(&self) -> &Self::Target {
+        &self.labels
+    }
+}
+
 impl Type<Postgres> for PgLTree {
     fn type_info() -> PgTypeInfo {
+        // Since `ltree` is enabled by an extension, it does not have a stable OID.
         PgTypeInfo::with_name("ltree")
     }
 }
@@ -120,7 +142,7 @@ impl<'r> Decode<'r, Postgres> for PgLTree {
                 let bytes = value.as_bytes()?;
                 let version = i8::from_le_bytes([bytes[0]; 1]);
                 if version != 1 {
-                    return Err(Box::new(Error::InvalidLtreeVersion));
+                    return Err(Box::new(PgLTreeParseError::InvalidLtreeVersion));
                 }
                 Ok(Self::from_str(std::str::from_utf8(&bytes[1..])?)?)
             }
diff --git a/sqlx-core/src/postgres/types/mod.rs b/sqlx-core/src/postgres/types/mod.rs
index c67658e043..26524f85d9 100644
--- a/sqlx-core/src/postgres/types/mod.rs
+++ b/sqlx-core/src/postgres/types/mod.rs
@@ -212,6 +212,7 @@ mod bit_vec;
 pub use array::PgHasArrayType;
 pub use interval::PgInterval;
 pub use ltree::PgLTree;
+pub use ltree::PgLTreeParseError;
 pub use money::PgMoney;
 pub use range::PgRange;
 

From 25d76349c134fced74cd52eedcb95442613e40de Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Sat, 12 Feb 2022 07:44:27 +0100
Subject: [PATCH 16/20] add tests

---
 sqlx-core/src/postgres/types/ltree.rs | 10 +++++++++-
 tests/postgres/setup.sql              |  3 +++
 tests/postgres/types.rs               | 16 ++++++++++++++--
 3 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index 6e2a512b0a..96f7de4f97 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -1,7 +1,9 @@
 use crate::decode::Decode;
 use crate::encode::{Encode, IsNull};
 use crate::error::BoxDynError;
-use crate::postgres::{PgArgumentBuffer, PgTypeInfo, PgValueFormat, PgValueRef, Postgres};
+use crate::postgres::{
+    PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres,
+};
 use crate::types::Type;
 use std::fmt::{self, Display, Formatter};
 use std::ops::Deref;
@@ -126,6 +128,12 @@ impl Type<Postgres> for PgLTree {
     }
 }
 
+impl PgHasArrayType for PgLTree {
+    fn array_type_info() -> PgTypeInfo {
+        PgTypeInfo::with_name("ltree[]")
+    }
+}
+
 impl Encode<'_, Postgres> for PgLTree {
     fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull {
         buf.extend(1i8.to_le_bytes());
diff --git a/tests/postgres/setup.sql b/tests/postgres/setup.sql
index 9818d139ba..d0abd16612 100644
--- a/tests/postgres/setup.sql
+++ b/tests/postgres/setup.sql
@@ -1,3 +1,6 @@
+-- https://www.postgresql.org/docs/current/ltree.html
+CREATE EXTENSION IF NOT EXISTS 'ltree';
+
 -- https://www.postgresql.org/docs/current/sql-createtype.html
 CREATE TYPE status AS ENUM ('new', 'open', 'closed');
 
diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs
index 0b58421448..003d94cac5 100644
--- a/tests/postgres/types.rs
+++ b/tests/postgres/types.rs
@@ -1,12 +1,11 @@
 extern crate time_ as time;
 
 use std::ops::Bound;
-#[cfg(feature = "decimal")]
-use std::str::FromStr;
 
 use sqlx::postgres::types::{PgInterval, PgMoney, PgRange};
 use sqlx::postgres::Postgres;
 use sqlx_test::{test_decode_type, test_prepared_type, test_type};
+use std::str::FromStr;
 
 test_type!(null<Option<i16>>(Postgres,
     "NULL::int2" == None::<i16>
@@ -513,3 +512,16 @@ test_prepared_type!(money<PgMoney>(Postgres, "123.45::money" == PgMoney(12345)))
 test_prepared_type!(money_vec<Vec<PgMoney>>(Postgres,
     "array[123.45,420.00,666.66]::money[]" == vec![PgMoney(12345), PgMoney(42000), PgMoney(66666)],
 ));
+
+test_type!(ltree<sqlx::postgres::types::PgLTree>(Postgres,
+    "'Foo.Bar.Baz.Quux'::ltree" == sqlx::postgres::types::PgLTree::from_str("Foo.Bar.Baz.Quux").unwrap(),
+    "'Alpha.Beta.Delta.Gamma'::ltree" == sqlx::postgres::types::PgLTree::from_iter(["Alpha", "Beta", "Delta", "Gamma"]).unwrap(),
+));
+
+test_type!(ltree_vec<Vec<sqlx::postgres::types::PgLTree>>(Postgres,
+    "['Foo.Bar.Baz.Quux', 'Alpha.Beta.Delta.Gamma']::ltree[]" ==
+        vec![
+            sqlx::postgres::types::PgLTree::from_str("Foo.Bar.Baz.Quux").unwrap(),
+            sqlx::postgres::types::PgLTree::from_iter(["Alpha", "Beta", "Delta", "Gamma"]).unwrap()
+        ]
+));

From 75fe431b43b76b1e540197d68562915fb94c9ce3 Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Sat, 12 Feb 2022 09:13:09 +0100
Subject: [PATCH 17/20] remove code from arguments

---
 sqlx-core/src/postgres/arguments.rs   | 20 --------------------
 sqlx-core/src/postgres/types/ltree.rs |  4 +++-
 2 files changed, 3 insertions(+), 21 deletions(-)

diff --git a/sqlx-core/src/postgres/arguments.rs b/sqlx-core/src/postgres/arguments.rs
index c94d4c45fe..9bd60dbbbd 100644
--- a/sqlx-core/src/postgres/arguments.rs
+++ b/sqlx-core/src/postgres/arguments.rs
@@ -1,5 +1,3 @@
-use std::fmt;
-use std::io::Write;
 use std::ops::{Deref, DerefMut};
 
 use crate::arguments::Arguments;
@@ -163,24 +161,6 @@ impl PgArgumentBuffer {
         self.extend_from_slice(&0_u32.to_be_bytes());
         self.type_holes.push((offset, type_name.clone()));
     }
-
-    pub(crate) fn extend_display<T: fmt::Display>(&mut self, value: T) {
-        struct VecFmt<'a>(&'a mut Vec<u8>);
-
-        impl<'a> fmt::Write for VecFmt<'a> {
-            fn write_char(&mut self, c: char) -> fmt::Result {
-                self.write_str(c.encode_utf8(&mut [0u8; 4]))
-            }
-
-            fn write_str(&mut self, s: &str) -> fmt::Result {
-                self.0.extend_from_slice(s.as_bytes());
-                Ok(())
-            }
-        }
-
-        write!(self.buffer, "{}", value)
-            .expect("Display implementation panicked while writing to PgArgumentBuffer");
-    }
 }
 
 impl Deref for PgArgumentBuffer {
diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index 96f7de4f97..f864aba861 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -6,6 +6,7 @@ use crate::postgres::{
 };
 use crate::types::Type;
 use std::fmt::{self, Display, Formatter};
+use std::io::Write;
 use std::ops::Deref;
 use std::str::FromStr;
 
@@ -137,7 +138,8 @@ impl PgHasArrayType for PgLTree {
 impl Encode<'_, Postgres> for PgLTree {
     fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull {
         buf.extend(1i8.to_le_bytes());
-        buf.extend_display(self);
+        write!(buf, "{}", self)
+            .expect("Display implementation panicked while writing to PgArgumentBuffer");
 
         IsNull::No
     }

From b5c8def72dbef5fd4eafedf7cedb3bc2174a9f42 Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Sat, 12 Feb 2022 09:27:20 +0100
Subject: [PATCH 18/20] fix array

---
 sqlx-core/src/postgres/types/ltree.rs | 2 +-
 sqlx-test/src/lib.rs                  | 1 +
 tests/postgres/types.rs               | 2 +-
 3 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index f864aba861..d3e525eb1f 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -131,7 +131,7 @@ impl Type<Postgres> for PgLTree {
 
 impl PgHasArrayType for PgLTree {
     fn array_type_info() -> PgTypeInfo {
-        PgTypeInfo::with_name("ltree[]")
+        PgTypeInfo::with_name("_ltree")
     }
 }
 
diff --git a/sqlx-test/src/lib.rs b/sqlx-test/src/lib.rs
index 052e157271..4efa9d298c 100644
--- a/sqlx-test/src/lib.rs
+++ b/sqlx-test/src/lib.rs
@@ -158,6 +158,7 @@ macro_rules! __test_prepared_type {
 
                 $(
                     let query = format!($sql, $text);
+                    println!("{query}");
 
                     let row = sqlx::query(&query)
                         .bind($value)
diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs
index 003d94cac5..e9dd35698e 100644
--- a/tests/postgres/types.rs
+++ b/tests/postgres/types.rs
@@ -519,7 +519,7 @@ test_type!(ltree<sqlx::postgres::types::PgLTree>(Postgres,
 ));
 
 test_type!(ltree_vec<Vec<sqlx::postgres::types::PgLTree>>(Postgres,
-    "['Foo.Bar.Baz.Quux', 'Alpha.Beta.Delta.Gamma']::ltree[]" ==
+    "array['Foo.Bar.Baz.Quux', 'Alpha.Beta.Delta.Gamma']::ltree[]" ==
         vec![
             sqlx::postgres::types::PgLTree::from_str("Foo.Bar.Baz.Quux").unwrap(),
             sqlx::postgres::types::PgLTree::from_iter(["Alpha", "Beta", "Delta", "Gamma"]).unwrap()

From 580148b994ee46cd2b7832fd501320599eff6495 Mon Sep 17 00:00:00 2001
From: Bastian Schubert <bastian.schubert@crosscard.com>
Date: Mon, 14 Feb 2022 11:30:31 +0100
Subject: [PATCH 19/20] fix setup isse

---
 tests/postgres/setup.sql | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/postgres/setup.sql b/tests/postgres/setup.sql
index d0abd16612..1a42d0b899 100644
--- a/tests/postgres/setup.sql
+++ b/tests/postgres/setup.sql
@@ -1,5 +1,5 @@
 -- https://www.postgresql.org/docs/current/ltree.html
-CREATE EXTENSION IF NOT EXISTS 'ltree';
+CREATE EXTENSION IF NOT EXISTS ltree;
 
 -- https://www.postgresql.org/docs/current/sql-createtype.html
 CREATE TYPE status AS ENUM ('new', 'open', 'closed');

From 74d075304625c68730a3a1baba0ef3ed0e0d1ffe Mon Sep 17 00:00:00 2001
From: Austin Bonander <austin@launchbadge.com>
Date: Tue, 15 Feb 2022 20:06:32 -0800
Subject: [PATCH 20/20] fix(postgres): disable `ltree` tests on Postgres 9.6

---
 .github/workflows/sqlx.yml            | 10 ++++++++++
 sqlx-core/src/postgres/types/ltree.rs | 10 ++++++++++
 tests/postgres/types.rs               |  6 ++++++
 3 files changed, 26 insertions(+)

diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml
index 44964b509b..f325cb7c3f 100644
--- a/.github/workflows/sqlx.yml
+++ b/.github/workflows/sqlx.yml
@@ -208,6 +208,10 @@ jobs:
           key: ${{ runner.os }}-postgres-${{ matrix.runtime }}-${{ matrix.tls }}-${{ hashFiles('**/Cargo.lock') }}
 
       - uses: actions-rs/cargo@v1
+        env:
+          # FIXME: needed to disable `ltree` tests in Postgres 9.6
+          # but `PgLTree` should just fall back to text format
+          RUSTFLAGS: --cfg postgres_${{ matrix.postgres }}
         with:
           command: build
           args: >
@@ -225,6 +229,9 @@ jobs:
             --features any,postgres,macros,all-types,runtime-${{ matrix.runtime }}-${{ matrix.tls }}
         env:
           DATABASE_URL: postgres://postgres:password@localhost:5432/sqlx
+          # FIXME: needed to disable `ltree` tests in Postgres 9.6
+          # but `PgLTree` should just fall back to text format
+          RUSTFLAGS: --cfg postgres_${{ matrix.postgres }}
 
       - uses: actions-rs/cargo@v1
         with:
@@ -234,6 +241,9 @@ jobs:
             --features any,postgres,macros,migrate,all-types,runtime-${{ matrix.runtime }}-${{ matrix.tls }}
         env:
           DATABASE_URL: postgres://postgres:password@localhost:5432/sqlx?sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt
+          # FIXME: needed to disable `ltree` tests in Postgres 9.6
+          # but `PgLTree` should just fall back to text format
+          RUSTFLAGS: --cfg postgres_${{ matrix.postgres }}
 
   mysql:
     name: MySQL
diff --git a/sqlx-core/src/postgres/types/ltree.rs b/sqlx-core/src/postgres/types/ltree.rs
index d3e525eb1f..23173f528e 100644
--- a/sqlx-core/src/postgres/types/ltree.rs
+++ b/sqlx-core/src/postgres/types/ltree.rs
@@ -27,6 +27,16 @@ pub enum PgLTreeParseError {
 ///
 /// See https://www.postgresql.org/docs/current/ltree.html
 ///
+/// ### Note: Requires Postgres 13+
+///
+/// This integration requires that the `ltree` type support the binary format in the Postgres
+/// wire protocol, which only became available in Postgres 13.
+/// ([Postgres 13.0 Release Notes, Additional Modules][https://www.postgresql.org/docs/13/release-13.html#id-1.11.6.11.5.14])
+///
+/// Ideally, SQLx's Postgres driver should support falling back to text format for types
+/// which don't have `typsend` and `typrecv` entries in `pg_type`, but that work still needs
+/// to be done.
+///
 /// ### Note: Extension Required
 /// The `ltree` extension is not enabled by default in Postgres. You will need to do so explicitly:
 ///
diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs
index e9dd35698e..21ebbe6120 100644
--- a/tests/postgres/types.rs
+++ b/tests/postgres/types.rs
@@ -513,11 +513,17 @@ test_prepared_type!(money_vec<Vec<PgMoney>>(Postgres,
     "array[123.45,420.00,666.66]::money[]" == vec![PgMoney(12345), PgMoney(42000), PgMoney(66666)],
 ));
 
+// FIXME: needed to disable `ltree` tests in Postgres 9.6
+// but `PgLTree` should just fall back to text format
+#[cfg(postgres_14)]
 test_type!(ltree<sqlx::postgres::types::PgLTree>(Postgres,
     "'Foo.Bar.Baz.Quux'::ltree" == sqlx::postgres::types::PgLTree::from_str("Foo.Bar.Baz.Quux").unwrap(),
     "'Alpha.Beta.Delta.Gamma'::ltree" == sqlx::postgres::types::PgLTree::from_iter(["Alpha", "Beta", "Delta", "Gamma"]).unwrap(),
 ));
 
+// FIXME: needed to disable `ltree` tests in Postgres 9.6
+// but `PgLTree` should just fall back to text format
+#[cfg(postgres_14)]
 test_type!(ltree_vec<Vec<sqlx::postgres::types::PgLTree>>(Postgres,
     "array['Foo.Bar.Baz.Quux', 'Alpha.Beta.Delta.Gamma']::ltree[]" ==
         vec![