Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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"] }
6 changes: 6 additions & 0 deletions psl/parser-database/src/attributes/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ pub(super) fn visit_model_field_default(
"Only @default(dbgenerated(\"...\")) can be used for Unsupported types.",
);
}
ScalarFieldType::Geometry(_) => {
ctx.push_attribute_validation_error("Only @default(dbgenerated(\"...\")) can be used for Geometry types.");
}
}
}

Expand Down Expand Up @@ -139,6 +142,9 @@ pub(super) fn visit_composite_field_default(
ScalarFieldType::Unsupported(_) => {
ctx.push_attribute_validation_error("Composite field of type `Unsupported` cannot have default values.")
}
ScalarFieldType::Geometry(_) => {
ctx.push_attribute_validation_error("Composite field of type `Geometry` cannot have default values.")
}
}
}

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
184 changes: 153 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::{self, Write as _},
};

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,95 @@ impl UnsupportedType {
}
}

/// OGC / PostGIS geometry subtype for [`ScalarFieldType::Geometry`].
#[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",
}
}
}

impl From<ast::GeometrySubtype> for GeometrySubtype {
fn from(s: ast::GeometrySubtype) -> Self {
match s {
ast::GeometrySubtype::Point => Self::Point,
ast::GeometrySubtype::LineString => Self::LineString,
ast::GeometrySubtype::Polygon => Self::Polygon,
ast::GeometrySubtype::MultiPoint => Self::MultiPoint,
ast::GeometrySubtype::MultiLineString => Self::MultiLineString,
ast::GeometrySubtype::MultiPolygon => Self::MultiPolygon,
ast::GeometrySubtype::GeometryCollection => Self::GeometryCollection,
ast::GeometrySubtype::Geometry => Self::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 {
/// 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",
};
let subtype = self.subtype.as_str();
match self.srid {
Some(srid) => format!("{base}({subtype},{srid})"),
None => format!("{base}({subtype})"),
}
}
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 All @@ -195,6 +288,8 @@ pub enum ScalarFieldType {
Extension(ExtensionTypeId),
/// A Prisma scalar type
BuiltInScalar(ScalarType),
/// PostGIS-style `Geometry(Point, 4326)` scalar
Geometry(GeometrySpec),
/// An `Unsupported("...")` type
Unsupported(UnsupportedType),
}
Expand Down Expand Up @@ -277,6 +372,11 @@ impl ScalarFieldType {
matches!(self, Self::BuiltInScalar(ScalarType::Decimal))
}

/// True if the field's type is `Geometry(...)`.
pub fn is_geometry(self) -> bool {
matches!(self, Self::Geometry(_))
}

/// 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 @@ -307,6 +407,13 @@ impl fmt::Display for DisplayScalarFieldType<'_> {
.expect("extension type id to have a name");
write!(f, "{}", self.db.interner.get(*name).unwrap())
}
ScalarFieldType::Geometry(spec) => {
write!(f, "Geometry({}", spec.subtype.as_str())?;
if let Some(srid) = spec.srid {
write!(f, ", {srid}")?;
}
f.write_char(')')
}
ScalarFieldType::Unsupported(ut) => {
write!(f, "Unsupported(\"{}\")", self.db.interner.get(ut.name).unwrap())
}
Expand Down Expand Up @@ -473,6 +580,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 +947,52 @@ 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::Geometry { subtype, srid, .. } => {
Ok(FieldType::Scalar(ScalarFieldType::Geometry(GeometrySpec {
subtype: (*subtype).into(),
srid: *srid,
spatial: PostgisSpatialKind::Geometry,
})))
}
ast::FieldType::Unsupported(name, _) => {
let unsupported = UnsupportedType::new(ctx.interner.intern(name));
return 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))))
Ok(FieldType::Scalar(ScalarFieldType::Unsupported(unsupported)))
}
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod native_types;
mod validations;

pub use native_types::{KnownPostgresType, PostgresType};
use parser_database::{ExtensionTypes, ScalarFieldType};
use parser_database::{ExtensionTypes, GeometrySpec, ScalarFieldType};

use crate::{
Configuration, Datasource, DatasourceConnectorData, PreviewFeature, ValidatedSchema,
Expand Down Expand Up @@ -74,14 +74,19 @@ pub const CAPABILITIES: ConnectorCapabilities = enumflags2::make_bitflags!(Conne
SupportsFiltersOnRelationsWithoutJoins |
LateralJoin |
SupportsDefaultInInsert |
PartialIndex
PartialIndex |
PostgisGeometry
});

pub struct PostgresDatamodelConnector;

const DATE_TIME_DEFAULT: KnownPostgresType = KnownPostgresType::Timestamp(Some(3));
const BYTES_DEFAULT: KnownPostgresType = KnownPostgresType::ByteA;

fn geometry_sql_column_type(spec: &GeometrySpec) -> String {
spec.postgres_sql_type()
}
Comment thread
lh0x00 marked this conversation as resolved.
Outdated

const SCALAR_TYPE_DEFAULTS: &[(ScalarType, KnownPostgresType)] = &[
(ScalarType::Int, KnownPostgresType::Integer),
(ScalarType::BigInt, KnownPostgresType::BigInt),
Expand Down Expand Up @@ -374,6 +379,10 @@ impl Connector for PostgresDatamodelConnector {
let native_type = PostgresType::Unknown(name.to_owned(), modifiers.to_vec());
return Some(NativeTypeInstance::new::<PostgresType>(native_type));
}
ScalarFieldType::Geometry(spec) => {
let native_type = PostgresType::Unknown(geometry_sql_column_type(spec), Vec::new());
return Some(NativeTypeInstance::new::<PostgresType>(native_type));
}
ScalarFieldType::CompositeType(_) | ScalarFieldType::Enum(_) | ScalarFieldType::Unsupported(_) => {
return None;
}
Expand Down
3 changes: 2 additions & 1 deletion psl/psl-core/src/datamodel_connector/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ macro_rules! capabilities {
($( $variant:ident $(,)? ),*) => {
#[derive(Debug, Clone, Copy, PartialEq)]
#[enumflags2::bitflags]
#[repr(u64)]
#[repr(u128)]
pub enum ConnectorCapability {
$(
$variant,
Expand Down Expand Up @@ -93,6 +93,7 @@ capabilities!(
AdvancedJsonNullability, // Connector distinguishes between their null type and JSON null.
UndefinedType, // Connector distinguishes `null` and `undefined`
DecimalType, // Connector supports Prisma Decimal type.
PostgisGeometry, // Connector supports first-class `Geometry(...)` (PostgreSQL / PostGIS).
BackwardCompatibleQueryRaw, // Temporary SQLite specific capability. Should be removed once https://github.com/prisma/prisma/issues/12784 is fixed,
OrderByNullsFirstLast, // Connector supports ORDER BY NULLS LAST/FIRST
FilteredInlineChildNestedToOneDisconnect, // Connector supports a filtered nested disconnect on both sides of a to-one relation.
Expand Down
2 changes: 2 additions & 0 deletions psl/psl-core/src/validate/validation_pipeline/validations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub(super) fn validate(ctx: &mut Context<'_>) {
for field in composite_type.fields() {
composite_types::validate_default_value(field, ctx);
fields::validate_native_type_arguments(field, ctx);
fields::validate_geometry_on_composite_field(field, ctx);
}
}
}
Expand Down Expand Up @@ -92,6 +93,7 @@ pub(super) fn validate(ctx: &mut Context<'_>) {

for field in model.scalar_fields() {
fields::validate_scalar_field_connector_specific(field, ctx);
fields::validate_geometry_field(field, ctx);
fields::validate_client_name(field.into(), &names, ctx);
fields::has_a_unique_default_constraint_name(field, &names, ctx);
fields::validate_native_type_arguments(field, ctx);
Expand Down
Loading
Loading