-
Notifications
You must be signed in to change notification settings - Fork 312
Native PostGIS Support for Prisma #5797
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lh0x00
wants to merge
7
commits into
prisma:main
Choose a base branch
from
lh0x00:feature/support-postgis-features
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
6efd1f0
feat(db): add PostGIS support for Prisma
lh0x00 3aa757a
feat(db): add Prisma-native querying support for PostGIS
lh0x00 f4695b4
feat(db): enhance PostGIS querying with Prisma-native patterns and up…
lh0x00 b644117
fix(postgis): address SRID None vs 0 edge cases and update operation …
lh0x00 be97d1e
Merge branch 'main' into feature/support-postgis-features
lh0x00 c90f1ee
feat(postgis): add Geometry/Geography scalars with spatial filters wh…
lh0x00 7e29da2
test(postgis): match capitalized rendering in migration assertions a…
lh0x00 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,11 @@ use either::Either; | |
| use enumflags2::bitflags; | ||
| use rustc_hash::FxHashMap as HashMap; | ||
| use schema_ast::ast::{self, EnumValueId, WithName}; | ||
| use std::{collections::BTreeMap, fmt}; | ||
| use serde::{Deserialize, Serialize}; | ||
| use std::{ | ||
| collections::BTreeMap, | ||
| fmt, | ||
| }; | ||
|
|
||
| pub(super) fn resolve_types(ctx: &mut Context<'_>) { | ||
| for ((file_id, top_id), top) in ctx.iter_tops() { | ||
|
|
@@ -184,6 +188,97 @@ impl UnsupportedType { | |
| } | ||
| } | ||
|
|
||
| /// OGC / PostGIS geometry subtype carried by `PostgresType::Postgis(...)` and surfaced via the | ||
| /// `@db.Geometry(...)` / `@db.Geography(...)` native attributes. The PSL keyword side uses the | ||
| /// unit [`ScalarType::Geometry`] / [`ScalarType::Geography`] variants. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] | ||
| pub enum GeometrySubtype { | ||
| /// `POINT` subtype. | ||
| Point, | ||
| /// `LINESTRING` subtype. | ||
| LineString, | ||
| /// `POLYGON` subtype. | ||
| Polygon, | ||
| /// `MULTIPOINT` subtype. | ||
| MultiPoint, | ||
| /// `MULTILINESTRING` subtype. | ||
| MultiLineString, | ||
| /// `MULTIPOLYGON` subtype. | ||
| MultiPolygon, | ||
| /// `GEOMETRYCOLLECTION` subtype. | ||
| GeometryCollection, | ||
| /// Unrestricted `GEOMETRY` subtype. | ||
| Geometry, | ||
| } | ||
|
|
||
| impl GeometrySubtype { | ||
| /// PSL spelling of the subtype (e.g. `Point`). | ||
| pub fn as_str(self) -> &'static str { | ||
| match self { | ||
| GeometrySubtype::Point => "Point", | ||
| GeometrySubtype::LineString => "LineString", | ||
| GeometrySubtype::Polygon => "Polygon", | ||
| GeometrySubtype::MultiPoint => "MultiPoint", | ||
| GeometrySubtype::MultiLineString => "MultiLineString", | ||
| GeometrySubtype::MultiPolygon => "MultiPolygon", | ||
| GeometrySubtype::GeometryCollection => "GeometryCollection", | ||
| GeometrySubtype::Geometry => "Geometry", | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// PostGIS base type for a [`GeometrySpec`] (`geometry` vs `geography` in PostgreSQL). | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] | ||
| pub enum PostgisSpatialKind { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would make more sense as separate |
||
| /// `geometry(...)` columns (planar). | ||
| #[default] | ||
| Geometry, | ||
| /// `geography(...)` columns (geodetic). | ||
| Geography, | ||
| } | ||
|
|
||
| /// Parameters for a `Geometry(subtype, srid?)` scalar field type. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] | ||
| pub struct GeometrySpec { | ||
| /// Geometry subtype (OGC / PostGIS). | ||
| pub subtype: GeometrySubtype, | ||
| /// Spatial reference ID; `None` when omitted in the schema (distinct from SRID 0 in the database). | ||
| pub srid: Option<i32>, | ||
| /// Whether the physical column uses PostGIS `geometry` or `geography`. | ||
| #[serde(default)] | ||
| pub spatial: PostgisSpatialKind, | ||
| } | ||
|
|
||
| impl GeometrySpec { | ||
| /// PSL scalar type name as it appears in the schema language: either `Geometry` or | ||
| /// `Geography`. The casing matches the keyword the user writes in the schema. | ||
| pub fn psl_type_name(&self) -> &'static str { | ||
| match self.spatial { | ||
| PostgisSpatialKind::Geometry => "Geometry", | ||
| PostgisSpatialKind::Geography => "Geography", | ||
| } | ||
| } | ||
|
|
||
| /// SQL column type for PostgreSQL / PostGIS (e.g. `geometry(Point,4326)` or `geography(Point,4326)`). | ||
| pub fn postgres_sql_type(&self) -> String { | ||
| let base = match self.spatial { | ||
| PostgisSpatialKind::Geometry => "geometry", | ||
| PostgisSpatialKind::Geography => "geography", | ||
| }; | ||
| // PostGIS rejects `geometry(Geometry)` as a column type — the unconstrained form is | ||
| // simply `geometry` (or `geography`). Only emit the parameter list when a concrete | ||
| // subtype or SRID is specified. | ||
| let bare_subtype = self.subtype == GeometrySubtype::Geometry; | ||
| let subtype = self.subtype.as_str(); | ||
| match (self.srid, bare_subtype) { | ||
| (None, true) => base.to_owned(), | ||
| (None, false) => format!("{base}({subtype})"), | ||
| (Some(srid), true) => format!("{base}(Geometry,{srid})"), | ||
| (Some(srid), false) => format!("{base}({subtype},{srid})"), | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /// The type of a scalar field, parsed and categorized. | ||
| #[derive(Debug, Clone, Copy, PartialEq)] | ||
| pub enum ScalarFieldType { | ||
|
|
@@ -277,6 +372,23 @@ impl ScalarFieldType { | |
| matches!(self, Self::BuiltInScalar(ScalarType::Decimal)) | ||
| } | ||
|
|
||
| /// True if the field's type is `Geometry` or `Geography` (any PostGIS spatial scalar). | ||
| pub fn is_geometry(self) -> bool { | ||
| matches!( | ||
| self, | ||
| Self::BuiltInScalar(ScalarType::Geometry) | Self::BuiltInScalar(ScalarType::Geography) | ||
| ) | ||
| } | ||
|
|
||
| /// PostGIS spatial kind discriminator (`Geometry` vs `Geography`) when the field type is a | ||
| /// PostGIS spatial scalar; `None` for any other field type. | ||
| pub fn postgis_spatial_kind(self) -> Option<PostgisSpatialKind> { | ||
| match self { | ||
| Self::BuiltInScalar(scalar) => scalar.postgis_spatial_kind(), | ||
| _ => None, | ||
| } | ||
| } | ||
|
|
||
| /// Display the field type as it would appear in the Prisma schema. | ||
| pub fn display<'a>(&'a self, db: &'a ParserDatabase) -> impl fmt::Display + 'a { | ||
| DisplayScalarFieldType { field_type: self, db } | ||
|
|
@@ -473,6 +585,10 @@ impl IndexAlgorithm { | |
| return true; | ||
| } | ||
|
|
||
| if r#type.is_geometry() { | ||
| return matches!(self, IndexAlgorithm::BTree | IndexAlgorithm::Gist); | ||
| } | ||
|
|
||
| match self { | ||
| IndexAlgorithm::BTree => true, | ||
| IndexAlgorithm::Hash => true, | ||
|
|
@@ -836,41 +952,46 @@ fn visit_enum<'db>(enm: &'db ast::Enum, ctx: &mut Context<'db>) { | |
| /// Either a structured, supported type, or an Err(unsupported) if the type name | ||
| /// does not match any we know of. | ||
| fn field_type<'db>(field: &'db ast::Field, ctx: &mut Context<'db>) -> Result<FieldType, &'db str> { | ||
| let supported = match &field.field_type { | ||
| ast::FieldType::Supported(ident) => &ident.name, | ||
| match &field.field_type { | ||
| ast::FieldType::Unsupported(name, _) => { | ||
| let unsupported = UnsupportedType::new(ctx.interner.intern(name)); | ||
| return Ok(FieldType::Scalar(ScalarFieldType::Unsupported(unsupported))); | ||
| Ok(FieldType::Scalar(ScalarFieldType::Unsupported(unsupported))) | ||
| } | ||
| }; | ||
|
|
||
| if let Some(tpe) = ScalarType::try_from_str(supported, false) { | ||
| return Ok(FieldType::Scalar(ScalarFieldType::BuiltInScalar(tpe))); | ||
| } | ||
|
|
||
| let supported_string_id = ctx.interner.intern(supported); | ||
| match ctx | ||
| .names | ||
| .tops | ||
| .get(&supported_string_id) | ||
| .map(|id| (id.0, id.1, &ctx.asts[*id])) | ||
| { | ||
| Some((file_id, ast::TopId::Model(model_id), ast::Top::Model(_))) => Ok(FieldType::Model((file_id, model_id))), | ||
| Some((file_id, ast::TopId::Enum(enum_id), ast::Top::Enum(_))) => { | ||
| Ok(FieldType::Scalar(ScalarFieldType::Enum((file_id, enum_id)))) | ||
| } | ||
| Some((file_id, ast::TopId::CompositeType(ctid), ast::Top::CompositeType(_))) => { | ||
| Ok(FieldType::Scalar(ScalarFieldType::CompositeType((file_id, ctid)))) | ||
| } | ||
| Some((_, _, ast::Top::Generator(_))) | Some((_, _, ast::Top::Source(_))) => unreachable!(), | ||
| None => { | ||
| if let Some(type_id) = ctx.extension_types().get_by_prisma_name(supported) { | ||
| Ok(FieldType::Scalar(ScalarFieldType::Extension(type_id))) | ||
| } else { | ||
| Err(supported) | ||
| ast::FieldType::Supported(ident) => { | ||
| let supported = ident.name.as_str(); | ||
|
|
||
| if let Some(tpe) = ScalarType::try_from_str(supported, false) { | ||
| return Ok(FieldType::Scalar(ScalarFieldType::BuiltInScalar(tpe))); | ||
| } | ||
|
|
||
|
|
||
| let supported_string_id = ctx.interner.intern(supported); | ||
| match ctx | ||
| .names | ||
| .tops | ||
| .get(&supported_string_id) | ||
| .map(|id| (id.0, id.1, &ctx.asts[*id])) | ||
| { | ||
| Some((file_id, ast::TopId::Model(model_id), ast::Top::Model(_))) => { | ||
| Ok(FieldType::Model((file_id, model_id))) | ||
| } | ||
| Some((file_id, ast::TopId::Enum(enum_id), ast::Top::Enum(_))) => { | ||
| Ok(FieldType::Scalar(ScalarFieldType::Enum((file_id, enum_id)))) | ||
| } | ||
| Some((file_id, ast::TopId::CompositeType(ctid), ast::Top::CompositeType(_))) => { | ||
| Ok(FieldType::Scalar(ScalarFieldType::CompositeType((file_id, ctid)))) | ||
| } | ||
| Some((_, _, ast::Top::Generator(_))) | Some((_, _, ast::Top::Source(_))) => unreachable!(), | ||
| None => { | ||
| if let Some(type_id) = ctx.extension_types().get_by_prisma_name(supported) { | ||
| Ok(FieldType::Scalar(ScalarFieldType::Extension(type_id))) | ||
| } else { | ||
| Err(supported) | ||
| } | ||
| } | ||
| _ => unreachable!(), | ||
| } | ||
| } | ||
| _ => unreachable!(), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -1554,6 +1675,13 @@ pub enum ScalarType { | |
| Json, | ||
| Bytes, | ||
| Decimal, | ||
| /// PostGIS `geometry(...)` planar scalar. The OGC subtype and SRID live in the native | ||
| /// attribute (`@db.Geometry(Subtype, SRID)`), mirroring how `String @db.VarChar(300)` | ||
| /// and `Decimal @db.Decimal(10, 2)` carry their parameters. | ||
| Geometry, | ||
| /// PostGIS `geography(...)` geodetic scalar. Same parameterization convention as | ||
| /// [`ScalarType::Geometry`] — the spatial kind discriminator is the variant itself. | ||
| Geography, | ||
| } | ||
|
|
||
| impl ScalarType { | ||
|
|
@@ -1569,6 +1697,18 @@ impl ScalarType { | |
| ScalarType::Json => "Json", | ||
| ScalarType::Bytes => "Bytes", | ||
| ScalarType::Decimal => "Decimal", | ||
| ScalarType::Geometry => "Geometry", | ||
| ScalarType::Geography => "Geography", | ||
| } | ||
| } | ||
|
|
||
| /// PostGIS spatial kind (`Geometry` vs `Geography`) for the two PostGIS-flavored scalar | ||
| /// variants; `None` for any non-spatial scalar. | ||
| pub fn postgis_spatial_kind(&self) -> Option<PostgisSpatialKind> { | ||
| match self { | ||
| ScalarType::Geometry => Some(PostgisSpatialKind::Geometry), | ||
| ScalarType::Geography => Some(PostgisSpatialKind::Geography), | ||
| _ => None, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -1590,6 +1730,8 @@ impl ScalarType { | |
| "json" => Some(ScalarType::Json), | ||
| "bytes" => Some(ScalarType::Bytes), | ||
| "decimal" => Some(ScalarType::Decimal), | ||
| "geometry" => Some(ScalarType::Geometry), | ||
| "geography" => Some(ScalarType::Geography), | ||
| _ => None, | ||
| }, | ||
| _ => match s { | ||
|
|
@@ -1602,6 +1744,8 @@ impl ScalarType { | |
| "Json" => Some(ScalarType::Json), | ||
| "Bytes" => Some(ScalarType::Bytes), | ||
| "Decimal" => Some(ScalarType::Decimal), | ||
| "Geometry" => Some(ScalarType::Geometry), | ||
| "Geography" => Some(ScalarType::Geography), | ||
| _ => None, | ||
| }, | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.