diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 280d1fc4f3..0dfbcbdf26 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -14,20 +14,20 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use latest Rust - run: rustup override set stable + - name: Setup Rust + run: | + rustup show active-toolchain || rustup toolchain install + rustup override set stable - uses: Swatinem/rust-cache@v2 - with: - key: sqlx-cli - run: > - cargo build - -p sqlx-cli - --bin sqlx - --release - --no-default-features - --features mysql,postgres,sqlite + cargo build + -p sqlx-cli + --bin sqlx + --release + --no-default-features + --features mysql,postgres,sqlite - uses: actions/upload-artifact@v4 with: @@ -63,9 +63,10 @@ jobs: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: mysql-examples - name: Todos (Setup) working-directory: examples/mysql/todos @@ -98,7 +99,7 @@ jobs: name: sqlx-cli path: /home/runner/.local/bin - - run: | + - run: | ls -R /home/runner/.local/bin chmod +x $HOME/.local/bin/sqlx echo $HOME/.local/bin >> $GITHUB_PATH @@ -106,9 +107,8 @@ jobs: - uses: actions/checkout@v4 - - uses: Swatinem/rust-cache@v2 - with: - key: pg-examples + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install - name: Axum Social with Tests (Setup) working-directory: examples/postgres/axum-social-with-tests @@ -217,9 +217,10 @@ jobs: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: sqlite-examples - name: TODOs (Setup) env: diff --git a/.github/workflows/sqlx-cli.yml b/.github/workflows/sqlx-cli.yml index 3aeb3d7d33..2250e0bfcb 100644 --- a/.github/workflows/sqlx-cli.yml +++ b/.github/workflows/sqlx-cli.yml @@ -15,8 +15,9 @@ jobs: steps: - uses: actions/checkout@v4 - - run: | - rustup update + - name: Setup Rust + run: | + rustup show active-toolchain || rustup toolchain install rustup component add clippy rustup toolchain install beta rustup component add --toolchain beta clippy @@ -40,18 +41,19 @@ jobs: matrix: # Note: macOS-latest uses M1 Silicon (ARM64) os: - - ubuntu-latest - # FIXME: migrations tests fail on Windows for whatever reason - # - windows-latest - - macOS-13 - - macOS-latest + - ubuntu-latest + # FIXME: migrations tests fail on Windows for whatever reason + # - windows-latest + - macOS-13 + - macOS-latest steps: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: ${{ runner.os }}-test - run: cargo test --manifest-path sqlx-cli/Cargo.toml @@ -85,12 +87,12 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use latest Rust - run: rustup override set stable + - name: Setup Rust + run: | + rustup show active-toolchain || rustup toolchain install + rustup override set stable - uses: Swatinem/rust-cache@v2 - with: - key: ${{ runner.os }}-cli - run: cargo build --manifest-path sqlx-cli/Cargo.toml --bin cargo-sqlx ${{ matrix.args }} diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml index 3f1f44d393..7f573a6349 100644 --- a/.github/workflows/sqlx.yml +++ b/.github/workflows/sqlx.yml @@ -10,7 +10,7 @@ on: jobs: format: name: Format - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - run: rustup component add rustfmt @@ -18,24 +18,25 @@ jobs: check: name: Check - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - runtime: [async-std, tokio] - tls: [native-tls, rustls, none] + runtime: [ async-std, tokio ] + tls: [ native-tls, rustls, none ] steps: - uses: actions/checkout@v4 - - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-check-${{ matrix.runtime }}-${{ matrix.tls }}" - - - run: | - rustup update + # Swatinem/rust-cache recommends setting up the rust toolchain first because it's used in cache keys + - name: Setup Rust + # https://blog.rust-lang.org/2025/03/02/Rustup-1.28.0.html + run: | + rustup show active-toolchain || rustup toolchain install rustup component add clippy rustup toolchain install beta rustup component add --toolchain beta clippy + - uses: Swatinem/rust-cache@v2 + - run: > cargo clippy --no-default-features @@ -52,26 +53,27 @@ jobs: check-minimal-versions: name: Check build using minimal versions - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - run: rustup update - - run: rustup toolchain install nightly + - name: Setup Rust + run: | + rustup show active-toolchain || rustup toolchain install + rustup toolchain install nightly - run: cargo +nightly generate-lockfile -Z minimal-versions - run: cargo build --all-features test: name: Unit Tests - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - uses: Swatinem/rust-cache@v2 - with: - key: ${{ runner.os }}-test + # https://blog.rust-lang.org/2025/03/02/Rustup-1.28.0.html + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install - - name: Install Rust - run: rustup update + - uses: Swatinem/rust-cache@v2 - name: Test sqlx-core run: > @@ -113,20 +115,22 @@ jobs: sqlite: name: SQLite - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - runtime: [async-std, tokio] - linking: [sqlite, sqlite-unbundled] + runtime: [ async-std, tokio ] + linking: [ sqlite, sqlite-unbundled ] needs: check steps: - uses: actions/checkout@v4 - run: mkdir /tmp/sqlite3-lib && wget -O /tmp/sqlite3-lib/ipaddr.so https://github.com/nalgeon/sqlean/releases/download/0.15.2/ipaddr.so + # https://blog.rust-lang.org/2025/03/02/Rustup-1.28.0.html + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-${{ matrix.linking }}-${{ matrix.runtime }}-${{ matrix.tls }}" - name: Install system sqlite library if: ${{ matrix.linking == 'sqlite-unbundled' }} @@ -179,19 +183,20 @@ jobs: postgres: name: Postgres - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - postgres: [17, 13] - runtime: [async-std, tokio] - tls: [native-tls, rustls-aws-lc-rs, rustls-ring, none] + postgres: [ 17, 13 ] + runtime: [ async-std, tokio ] + tls: [ native-tls, rustls-aws-lc-rs, rustls-ring, none ] needs: check steps: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-postgres-${{ matrix.runtime }}-${{ matrix.tls }}" - env: # FIXME: needed to disable `ltree` tests in Postgres 9.6 @@ -279,19 +284,20 @@ jobs: mysql: name: MySQL - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - mysql: [8] - runtime: [async-std, tokio] - tls: [native-tls, rustls-aws-lc-rs, rustls-ring, none] + mysql: [ 8 ] + runtime: [ async-std, tokio ] + tls: [ native-tls, rustls-aws-lc-rs, rustls-ring, none ] needs: check steps: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-mysql-${{ matrix.runtime }}-${{ matrix.tls }}" - run: cargo build --features mysql,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} @@ -367,19 +373,20 @@ jobs: mariadb: name: MariaDB - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - mariadb: [verylatest, 11_4, 10_11, 10_4] - runtime: [async-std, tokio] - tls: [native-tls, rustls-aws-lc-rs, rustls-ring, none] + mariadb: [ verylatest, 11_4, 10_11, 10_4 ] + runtime: [ async-std, tokio ] + tls: [ native-tls, rustls-aws-lc-rs, rustls-ring, none ] needs: check steps: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-mysql-${{ matrix.runtime }}-${{ matrix.tls }}" - run: cargo build --features mysql,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} diff --git a/ci.db b/ci.db deleted file mode 100644 index cc158a7280..0000000000 Binary files a/ci.db and /dev/null differ diff --git a/sqlx-postgres/src/type_checking.rs b/sqlx-postgres/src/type_checking.rs index 5758c264a1..c82fd62187 100644 --- a/sqlx-postgres/src/type_checking.rs +++ b/sqlx-postgres/src/type_checking.rs @@ -42,6 +42,8 @@ impl_type_checking!( sqlx::postgres::types::PgPath, + sqlx::postgres::types::PgPolygon, + #[cfg(feature = "uuid")] sqlx::types::Uuid, diff --git a/sqlx-postgres/src/types/geometry/mod.rs b/sqlx-postgres/src/types/geometry/mod.rs index f67846fef2..1437d72c5c 100644 --- a/sqlx-postgres/src/types/geometry/mod.rs +++ b/sqlx-postgres/src/types/geometry/mod.rs @@ -3,3 +3,4 @@ pub mod line; pub mod line_segment; pub mod path; pub mod point; +pub mod polygon; diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs new file mode 100644 index 0000000000..500c9933e9 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -0,0 +1,363 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::{PgPoint, Type}; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::bytes::Buf; +use sqlx_core::Error; +use std::mem; +use std::str::FromStr; + +const BYTE_WIDTH: usize = mem::size_of::(); + +/// ## Postgres Geometric Polygon type +/// +/// Description: Polygon (similar to closed polygon) +/// Representation: `((x1,y1),...)` +/// +/// Polygons are represented by lists of points (the vertexes of the polygon). Polygons are very similar to closed paths; the essential semantic difference is that a polygon is considered to include the area within it, while a path is not. +/// An important implementation difference between polygons and paths is that the stored representation of a polygon includes its smallest bounding box. This speeds up certain search operations, although computing the bounding box adds overhead while constructing new polygons. +/// Values of type polygon are specified using any of the following syntaxes: +/// +/// ```text +/// ( ( x1 , y1 ) , ... , ( xn , yn ) ) +/// ( x1 , y1 ) , ... , ( xn , yn ) +/// ( x1 , y1 , ... , xn , yn ) +/// x1 , y1 , ... , xn , yn +/// ``` +/// +/// where the points are the end points of the line segments comprising the boundary of the polygon. +/// +/// Seeh ttps://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-POLYGON +#[derive(Debug, Clone, PartialEq)] +pub struct PgPolygon { + pub points: Vec, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +struct Header { + length: usize, +} + +impl Type for PgPolygon { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("polygon") + } +} + +impl PgHasArrayType for PgPolygon { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_polygon") + } +} + +impl<'r> Decode<'r, Postgres> for PgPolygon { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgPolygon::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgPolygon::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgPolygon { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("polygon")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgPolygon { + type Err = Error; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); + let parts = sanitised.split(',').collect::>(); + + let mut points = vec![]; + + if parts.len() % 2 != 0 { + return Err(Error::Decode( + format!("Unmatched pair in POLYGON: {}", s).into(), + )); + } + + for chunk in parts.chunks_exact(2) { + if let [x_str, y_str] = chunk { + let x = parse_float_from_str(x_str, "could not get x")?; + let y = parse_float_from_str(y_str, "could not get y")?; + + let point = PgPoint { x, y }; + points.push(point); + } + } + + if !points.is_empty() { + return Ok(PgPolygon { points }); + } + + Err(Error::Decode( + format!("could not get polygon from {}", s).into(), + )) + } +} + +impl PgPolygon { + fn header(&self) -> Header { + Header { + length: self.points.len(), + } + } + + fn from_bytes(mut bytes: &[u8]) -> Result { + let header = Header::try_read(&mut bytes)?; + + if bytes.len() != header.data_size() { + return Err(format!( + "expected {} bytes after header, got {}", + header.data_size(), + bytes.len() + ) + .into()); + } + + if bytes.len() % BYTE_WIDTH * 2 != 0 { + return Err(format!( + "data length not divisible by pairs of {BYTE_WIDTH}: {}", + bytes.len() + ) + .into()); + } + + let mut out_points = Vec::with_capacity(bytes.len() / (BYTE_WIDTH * 2)); + while bytes.has_remaining() { + let point = PgPoint { + x: bytes.get_f64(), + y: bytes.get_f64(), + }; + out_points.push(point) + } + Ok(PgPolygon { points: out_points }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), BoxDynError> { + let header = self.header(); + buff.reserve(header.data_size()); + header.try_write(buff)?; + + for point in &self.points { + buff.extend_from_slice(&point.x.to_be_bytes()); + buff.extend_from_slice(&point.y.to_be_bytes()); + } + Ok(()) + } + + #[cfg(test)] + fn serialize_to_vec(&self) -> Vec { + let mut buff = PgArgumentBuffer::default(); + self.serialize(&mut buff).unwrap(); + buff.to_vec() + } +} + +impl Header { + const HEADER_WIDTH: usize = mem::size_of::() + mem::size_of::(); + + fn data_size(&self) -> usize { + self.length * BYTE_WIDTH * 2 + } + + fn try_read(buf: &mut &[u8]) -> Result { + if buf.len() < Self::HEADER_WIDTH { + return Err(format!( + "expected polygon data to contain at least {} bytes, got {}", + Self::HEADER_WIDTH, + buf.len() + )); + } + + let length = buf.get_i32(); + + let length = usize::try_from(length).ok().ok_or_else(|| { + format!( + "received polygon with length: {length}. Expected length between 0 and {}", + usize::MAX + ) + })?; + + Ok(Self { length }) + } + + fn try_write(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> { + let length = i32::try_from(self.length).map_err(|_| { + format!( + "polygon length exceeds allowed maximum ({} > {})", + self.length, + i32::MAX + ) + })?; + + buff.extend(length.to_be_bytes()); + + Ok(()) + } +} + +fn parse_float_from_str(s: &str, error_msg: &str) -> Result { + s.parse().map_err(|_| Error::Decode(error_msg.into())) +} + +#[cfg(test)] +mod polygon_tests { + + use std::str::FromStr; + + use crate::types::PgPoint; + + use super::PgPolygon; + + const POLYGON_BYTES: &[u8] = &[ + 0, 0, 0, 12, 192, 0, 0, 0, 0, 0, 0, 0, 192, 8, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, + 0, 192, 8, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, 0, 63, + 240, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, + 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 192, + 8, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, 0, 0, 0, 192, 8, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 191, + 240, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, + 0, 0, 0, + ]; + + #[test] + fn can_deserialise_polygon_type_bytes() { + let polygon = PgPolygon::from_bytes(POLYGON_BYTES).unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![ + PgPoint { x: -2., y: -3. }, + PgPoint { x: -1., y: -3. }, + PgPoint { x: -1., y: -1. }, + PgPoint { x: 1., y: 1. }, + PgPoint { x: 1., y: 3. }, + PgPoint { x: 2., y: 3. }, + PgPoint { x: 2., y: -3. }, + PgPoint { x: 1., y: -3. }, + PgPoint { x: 1., y: 0. }, + PgPoint { x: -1., y: 0. }, + PgPoint { x: -1., y: -2. }, + PgPoint { x: -2., y: -2. } + ] + } + ) + } + + #[test] + fn can_deserialise_polygon_type_str_first_syntax() { + let polygon = PgPolygon::from_str("[( 1, 2), (3, 4 )]").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_polygon_type_str_second_syntax() { + let polygon = PgPolygon::from_str("(( 1, 2), (3, 4 ))").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn cannot_deserialise_polygon_type_str_uneven_points_first_syntax() { + let input_str = "[( 1, 2), (3)]"; + let polygon = PgPolygon::from_str(input_str); + + assert!(polygon.is_err()); + + if let Err(err) = polygon { + assert_eq!( + err.to_string(), + format!("error occurred while decoding: Unmatched pair in POLYGON: {input_str}") + ) + } + } + + #[test] + fn cannot_deserialise_polygon_type_str_invalid_numbers() { + let input_str = "[( 1, 2), (2, three)]"; + let polygon = PgPolygon::from_str(input_str); + + assert!(polygon.is_err()); + + if let Err(err) = polygon { + assert_eq!( + err.to_string(), + format!("error occurred while decoding: could not get y") + ) + } + } + + #[test] + fn can_deserialise_polygon_type_str_third_syntax() { + let polygon = PgPolygon::from_str("(1, 2), (3, 4 )").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_polygon_type_str_fourth_syntax() { + let polygon = PgPolygon::from_str("1, 2, 3, 4").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_polygon_type_str_float() { + let polygon = PgPolygon::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1.1, y: 2.2 }, PgPoint { x: 3.3, y: 4.4 }] + } + ); + } + + #[test] + fn can_serialise_polygon_type() { + let polygon = PgPolygon { + points: vec![ + PgPoint { x: -2., y: -3. }, + PgPoint { x: -1., y: -3. }, + PgPoint { x: -1., y: -1. }, + PgPoint { x: 1., y: 1. }, + PgPoint { x: 1., y: 3. }, + PgPoint { x: 2., y: 3. }, + PgPoint { x: 2., y: -3. }, + PgPoint { x: 1., y: -3. }, + PgPoint { x: 1., y: 0. }, + PgPoint { x: -1., y: 0. }, + PgPoint { x: -1., y: -2. }, + PgPoint { x: -2., y: -2. }, + ], + }; + assert_eq!(polygon.serialize_to_vec(), POLYGON_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 5d684c969e..550ce62929 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -26,6 +26,7 @@ //! | [`PgLSeg`] | LSEG | //! | [`PgBox`] | BOX | //! | [`PgPath`] | PATH | +//! | [`PgPolygon`] | POLYGON | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., @@ -265,6 +266,7 @@ pub use geometry::line::PgLine; pub use geometry::line_segment::PgLSeg; pub use geometry::path::PgPath; pub use geometry::point::PgPoint; +pub use geometry::polygon::PgPolygon; pub use geometry::r#box::PgBox; pub use hstore::PgHstore; pub use interval::PgInterval; diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 0d15caf8de..d88e1657c1 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -530,6 +530,15 @@ test_type!(path(Postgres, "path('[(1.0, 2.0), (3.0,4.0)]')" == sqlx::postgres::types::PgPath { closed: false, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]}, )); +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(polygon(Postgres, + "polygon('((-2,-3),(-1,-3),(-1,-1),(1,1),(1,3),(2,3),(2,-3),(1,-3),(1,0),(-1,0),(-1,-2),(-2,-2))')" ~= sqlx::postgres::types::PgPolygon { points: vec![ + sqlx::postgres::types::PgPoint { x: -2., y: -3. }, sqlx::postgres::types::PgPoint { x: -1., y: -3. }, sqlx::postgres::types::PgPoint { x: -1., y: -1. }, sqlx::postgres::types::PgPoint { x: 1., y: 1. }, + sqlx::postgres::types::PgPoint { x: 1., y: 3. }, sqlx::postgres::types::PgPoint { x: 2., y: 3. }, sqlx::postgres::types::PgPoint { x: 2., y: -3. }, sqlx::postgres::types::PgPoint { x: 1., y: -3. }, + sqlx::postgres::types::PgPoint { x: 1., y: 0. }, sqlx::postgres::types::PgPoint { x: -1., y: 0. }, sqlx::postgres::types::PgPoint { x: -1., y: -2. }, sqlx::postgres::types::PgPoint { x: -2., y: -2. }, + ]}, +)); + #[cfg(feature = "rust_decimal")] test_type!(decimal(Postgres, "0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(),