Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions psl/parser-database/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ enumflags2.workspace = true
itertools.workspace = true
either.workspace = true
rustc-hash.workspace = true
serde = { workspace = true, features = ["derive"] }
2 changes: 1 addition & 1 deletion psl/parser-database/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ fn resolve_composite_type_attributes<'db>(

ctx.visit_attributes((ctid.0, (ctid.1, field_id)));

if let ScalarFieldType::BuiltInScalar(_scalar_type) = r#type {
if let ScalarFieldType::BuiltInScalar(_) = r#type {
// native type attributes
if let Some((datasource_name, type_name, args)) = ctx.visit_datasource_scoped() {
native_types::visit_composite_type_field_native_type_attribute(
Expand Down
14 changes: 14 additions & 0 deletions psl/parser-database/src/attributes/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@ fn validate_model_builtin_scalar_type_default(
field_id: (crate::ModelId, ast::FieldId),
ctx: &mut Context<'_>,
) {
// PostGIS spatial scalars only accept `@default(dbgenerated(...))` (already handled by the
// caller above). Reject everything else with a stable error matching the previous behaviour.
if matches!(scalar_type, ScalarType::Geometry | ScalarType::Geography) {
ctx.push_attribute_validation_error("Only @default(dbgenerated(\"...\")) can be used for Geometry types.");
return;
}
Comment thread
lh0x00 marked this conversation as resolved.

let arity = ctx.asts[field_id.0][field_id.1].arity;
match (scalar_type, value) {
// Functions
Expand Down Expand Up @@ -251,6 +258,13 @@ fn validate_composite_builtin_scalar_type_default(
field_arity: ast::FieldArity,
ctx: &mut Context<'_>,
) {
// PostGIS spatial scalars cannot have defaults on composite fields (mirrors the previous
// top-level rejection arm that lived on `ScalarFieldType::Geometry`).
if matches!(scalar_type, ScalarType::Geometry | ScalarType::Geography) {
ctx.push_attribute_validation_error("Composite field of type `Geometry` cannot have default values.");
return;
}

match (scalar_type, value) {
// Functions
(ScalarType::String, ast::Expression::Function(funcname, funcargs, _)) if funcname == FN_ULID => {
Expand Down
4 changes: 2 additions & 2 deletions psl/parser-database/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ pub use relations::{ManyToManyRelationId, ReferentialAction, RelationId};
use schema_ast::ast::{GeneratorConfig, SourceConfig};
pub use schema_ast::{SourceFile, ast};
pub use types::{
IndexAlgorithm, IndexFieldPath, IndexType, OperatorClass, RelationFieldId, ScalarFieldId, ScalarFieldType,
ScalarType, SortOrder, WhereClause, WhereCondition, WhereValue,
GeometrySpec, GeometrySubtype, IndexAlgorithm, IndexFieldPath, IndexType, OperatorClass, PostgisSpatialKind,
RelationFieldId, ScalarFieldId, ScalarFieldType, ScalarType, SortOrder, WhereClause, WhereCondition, WhereValue,
};

/// ParserDatabase is a container for a Schema AST, together with information
Expand Down
206 changes: 175 additions & 31 deletions psl/parser-database/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would make more sense as separate Geometry/Geography types like they are in PostGIS

/// `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})"),
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// The type of a scalar field, parsed and categorized.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ScalarFieldType {
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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!(),
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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,
}
}

Expand All @@ -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 {
Expand All @@ -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,
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ macro_rules! reachable_only_with_capability {
#[inline(always)]
#[allow(dead_code)] // not used if more than one connector is built
const fn check_comptime_capability(capabilities: ConnectorCapabilities, cap: ConnectorCapability) -> bool {
(capabilities.bits_c() & (cap as u64)) > 0
(capabilities.bits_c() & (cap as u128)) > 0
}

#[inline(always)]
Expand Down
4 changes: 3 additions & 1 deletion psl/psl-core/src/builtin_connectors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ pub use mssql_datamodel_connector::{MsSqlType, MsSqlTypeParameter};
#[cfg(feature = "mysql")]
pub use mysql_datamodel_connector::MySqlType;
#[cfg(feature = "postgresql")]
pub use postgres_datamodel_connector::{KnownPostgresType, PostgresDatasourceProperties, PostgresType};
pub use postgres_datamodel_connector::{
GeometryNativeArgs, KnownPostgresType, PostgisNativeType, PostgresDatasourceProperties, PostgresType,
};

mod capabilities_support;
#[cfg(feature = "mongodb")]
Expand Down
Loading