Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -6,9 +6,13 @@
};
use either::Either;
use enumflags2::bitflags;
use rustc_hash::FxHashMap as HashMap;

Check warning on line 9 in psl/parser-database/src/types.rs

View workflow job for this annotation

GitHub Actions / run lints and formatting checks

Diff in /home/runner/work/prisma-engines/prisma-engines/psl/parser-database/src/types.rs
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 @@
}
}

/// 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 @@
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 @@
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 @@
/// 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) {

Check warning on line 963 in psl/parser-database/src/types.rs

View workflow job for this annotation

GitHub Actions / run lints and formatting checks

Diff in /home/runner/work/prisma-engines/prisma-engines/psl/parser-database/src/types.rs
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 @@
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 @@
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 @@
"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 @@
"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
Loading