Add PostGIS GeoJSON support

Works for GET, POST, PATCH, PUT, DELETE, RPC.
This commit is contained in:
steve-chavez
2022-06-22 22:16:44 -05:00
committed by Steve Chavez
parent 86c40e8df9
commit 870f7a39b0
13 changed files with 300 additions and 14 deletions
+5
View File
@@ -29,6 +29,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2268, Allow returning XML from single-column queries - @fjf2002 - #2268, Allow returning XML from single-column queries - @fjf2002
- #2300, RPC POST for function w/single unnamed XML param #2300 - @fjf2002 - #2300, RPC POST for function w/single unnamed XML param #2300 - @fjf2002
- #1959, Bulk update with PATCH - @steve-chavez - #1959, Bulk update with PATCH - @steve-chavez
- #1564, Allow geojson output by specifying the `Accept: application/geo+json` media type - @steve-chavez
+ Requires postgis >= 3.0
+ Works for GET, RPC, POST/PATCH/DELETE with `Prefer: return=representation`.
+ Resource embedding works and the embedded rows will go into the `properties` key
+ In case of multiple geometries in the same table, you can choose which one will go into the `geometry` key with the usual `?select` query parameter.
### Fixed ### Fixed
+6 -6
View File
@@ -46,12 +46,12 @@ let
postgresqlVersions = postgresqlVersions =
[ [
{ name = "postgresql-14"; postgresql = pkgs.postgresql_14; } { name = "postgresql-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis ]); }
{ name = "postgresql-13"; postgresql = pkgs.postgresql_13; } { name = "postgresql-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis ]); }
{ name = "postgresql-12"; postgresql = pkgs.postgresql_12; } { name = "postgresql-12"; postgresql = pkgs.postgresql_12.withPackages (p: [ p.postgis ]); }
{ name = "postgresql-11"; postgresql = pkgs.postgresql_11; } { name = "postgresql-11"; postgresql = pkgs.postgresql_11.withPackages (p: [ p.postgis ]); }
{ name = "postgresql-10"; postgresql = pkgs.postgresql_10; } { name = "postgresql-10"; postgresql = pkgs.postgresql_10.withPackages (p: [ p.postgis ]); }
{ name = "postgresql-9.6"; postgresql = pkgs.postgresql_9_6; } { name = "postgresql-9.6"; postgresql = pkgs.postgresql_9_6.withPackages (p: [ p.postgis ]); }
]; ];
patches = patches =
+1
View File
@@ -199,6 +199,7 @@ test-suite spec
Feature.Query.JsonOperatorSpec Feature.Query.JsonOperatorSpec
Feature.Query.MultipleSchemaSpec Feature.Query.MultipleSchemaSpec
Feature.Query.ErrorSpec Feature.Query.ErrorSpec
Feature.Query.PostGISSpec
Feature.Query.QueryLimitedSpec Feature.Query.QueryLimitedSpec
Feature.Query.QuerySpec Feature.Query.QuerySpec
Feature.Query.RangeSpec Feature.Query.RangeSpec
+3
View File
@@ -271,6 +271,7 @@ handleRead headersOnly identifier context@RequestContext{..} = do
(shouldCount iPreferCount) (shouldCount iPreferCount)
(iAcceptContentType == CTTextCSV) (iAcceptContentType == CTTextCSV)
(iAcceptContentType == CTTextXML) (iAcceptContentType == CTTextXML)
(iAcceptContentType == CTGeoJSON)
bField bField
configDbPreparedStatements configDbPreparedStatements
@@ -464,6 +465,7 @@ handleInvoke invMethod proc context@RequestContext{..} = do
(iAcceptContentType == CTSingularJSON) (iAcceptContentType == CTSingularJSON)
(iAcceptContentType == CTTextCSV) (iAcceptContentType == CTTextCSV)
(iAcceptContentType == CTTextXML) (iAcceptContentType == CTTextXML)
(iAcceptContentType == CTGeoJSON)
(iPreferParameters == Just MultipleObjects) (iPreferParameters == Just MultipleObjects)
bField bField
(configDbPreparedStatements ctxConfig) (configDbPreparedStatements ctxConfig)
@@ -551,6 +553,7 @@ writeQuery mutation identifier@QualifiedIdentifier{..} isInsert pkCols context@R
(iAcceptContentType ctxApiRequest == CTSingularJSON) (iAcceptContentType ctxApiRequest == CTSingularJSON)
isInsert isInsert
(iAcceptContentType ctxApiRequest == CTTextCSV) (iAcceptContentType ctxApiRequest == CTTextCSV)
(iAcceptContentType ctxApiRequest == CTGeoJSON)
(iPreferRepresentation ctxApiRequest) (iPreferRepresentation ctxApiRequest)
pkCols pkCols
(configDbPreparedStatements ctxConfig) (configDbPreparedStatements ctxConfig)
+3
View File
@@ -18,6 +18,7 @@ import Protolude
data ContentType data ContentType
= CTApplicationJSON = CTApplicationJSON
| CTSingularJSON | CTSingularJSON
| CTGeoJSON
| CTTextCSV | CTTextCSV
| CTTextPlain | CTTextPlain
| CTTextXML | CTTextXML
@@ -40,6 +41,7 @@ toHeader ct = (hContentType, toMime ct <> charset)
-- | Convert from ContentType to a ByteString representing the mime type -- | Convert from ContentType to a ByteString representing the mime type
toMime :: ContentType -> ByteString toMime :: ContentType -> ByteString
toMime CTApplicationJSON = "application/json" toMime CTApplicationJSON = "application/json"
toMime CTGeoJSON = "application/geo+json"
toMime CTTextCSV = "text/csv" toMime CTTextCSV = "text/csv"
toMime CTTextPlain = "text/plain" toMime CTTextPlain = "text/plain"
toMime CTTextXML = "text/xml" toMime CTTextXML = "text/xml"
@@ -55,6 +57,7 @@ decodeContentType :: BS.ByteString -> ContentType
decodeContentType ct = decodeContentType ct =
case BS.takeWhile (/= BS.c2w ';') ct of case BS.takeWhile (/= BS.c2w ';') ct of
"application/json" -> CTApplicationJSON "application/json" -> CTApplicationJSON
"application/geo+json" -> CTGeoJSON
"text/csv" -> CTTextCSV "text/csv" -> CTTextCSV
"text/plain" -> CTTextPlain "text/plain" -> CTTextPlain
"text/xml" -> CTTextXML "text/xml" -> CTTextXML
+4
View File
@@ -11,6 +11,7 @@ module PostgREST.Query.SqlFragment
, SqlFragment , SqlFragment
, asBinaryF , asBinaryF
, asCsvF , asCsvF
, asGeoJsonF
, asJsonF , asJsonF
, asJsonSingleF , asJsonSingleF
, asXmlF , asXmlF
@@ -182,6 +183,9 @@ asJsonSingleF returnsScalar
asXmlF :: FieldName -> SqlFragment asXmlF :: FieldName -> SqlFragment
asXmlF fieldName = "coalesce(xmlagg(_postgrest_t." <> pgFmtIdent fieldName <> "), '')" asXmlF fieldName = "coalesce(xmlagg(_postgrest_t." <> pgFmtIdent fieldName <> "), '')"
asGeoJsonF :: SqlFragment
asGeoJsonF = "json_build_object('type', 'FeatureCollection', 'features', coalesce(json_agg(ST_AsGeoJSON(_postgrest_t)::json), '[]'))"
asBinaryF :: FieldName -> SqlFragment asBinaryF :: FieldName -> SqlFragment
asBinaryF fieldName = "coalesce(string_agg(_postgrest_t." <> pgFmtIdent fieldName <> ", ''), '')" asBinaryF fieldName = "coalesce(string_agg(_postgrest_t." <> pgFmtIdent fieldName <> ", ''), '')"
+9 -6
View File
@@ -43,10 +43,10 @@ import Protolude
-} -}
type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString, Either Error [GucHeader], Either Error (Maybe Status)) type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString, Either Error [GucHeader], Either Error (Maybe Status))
createWriteStatement :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> Bool -> createWriteStatement :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> Bool -> Bool ->
PreferRepresentation -> [Text] -> Bool -> PreferRepresentation -> [Text] -> Bool ->
SQL.Statement () ResultsWithCount SQL.Statement () ResultsWithCount
createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys = createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv asGeoJson rep pKeys =
SQL.dynamicallyParameterized snippet decodeStandard SQL.dynamicallyParameterized snippet decodeStandard
where where
snippet = snippet =
@@ -74,6 +74,7 @@ createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys
bodyF bodyF
| rep /= Full = "''" | rep /= Full = "''"
| asCsv = asCsvF | asCsv = asCsvF
| asGeoJson = asGeoJsonF
| wantSingle = asJsonSingleF False | wantSingle = asJsonSingleF False
| otherwise = asJsonF False | otherwise = asJsonF False
@@ -86,9 +87,9 @@ createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys
decodeStandard = decodeStandard =
fromMaybe (Nothing, 0, [], mempty, Right [], Right Nothing) <$> HD.rowMaybe standardRow fromMaybe (Nothing, 0, [], mempty, Right [], Right Nothing) <$> HD.rowMaybe standardRow
createReadStatement :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool -> createReadStatement :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool ->
SQL.Statement () ResultsWithCount SQL.Statement () ResultsWithCount
createReadStatement selectQuery countQuery isSingle countTotal asCsv asXml binaryField = createReadStatement selectQuery countQuery isSingle countTotal asCsv asXml asGeoJson binaryField =
SQL.dynamicallyParameterized snippet decodeStandard SQL.dynamicallyParameterized snippet decodeStandard
where where
snippet = snippet =
@@ -109,6 +110,7 @@ createReadStatement selectQuery countQuery isSingle countTotal asCsv asXml binar
bodyF bodyF
| asCsv = asCsvF | asCsv = asCsvF
| isSingle = asJsonSingleF False | isSingle = asJsonSingleF False
| asGeoJson = asGeoJsonF
| isJust binaryField && asXml = asXmlF $ fromJust binaryField | isJust binaryField && asXml = asXmlF $ fromJust binaryField
| isJust binaryField = asBinaryF $ fromJust binaryField | isJust binaryField = asBinaryF $ fromJust binaryField
| otherwise = asJsonF False | otherwise = asJsonF False
@@ -130,9 +132,9 @@ standardRow = (,,,,,) <$> nullableColumn HD.int8 <*> column HD.int8
type ProcResults = (Maybe Int64, Int64, ByteString, Either Error [GucHeader], Either Error (Maybe Status)) type ProcResults = (Maybe Int64, Int64, ByteString, Either Error [GucHeader], Either Error (Maybe Status))
callProcStatement :: Bool -> Bool -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool -> callProcStatement :: Bool -> Bool -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool ->
SQL.Statement () ProcResults SQL.Statement () ProcResults
callProcStatement returnsScalar returnsSingle callProcQuery selectQuery countQuery countTotal asSingle asCsv asXml multObjects binaryField = callProcStatement returnsScalar returnsSingle callProcQuery selectQuery countQuery countTotal asSingle asCsv asXml asGeoJson multObjects binaryField =
SQL.dynamicallyParameterized snippet decodeProc SQL.dynamicallyParameterized snippet decodeProc
where where
snippet = snippet =
@@ -152,6 +154,7 @@ callProcStatement returnsScalar returnsSingle callProcQuery selectQuery countQue
bodyF bodyF
| asSingle = asJsonSingleF returnsScalar | asSingle = asJsonSingleF returnsScalar
| asCsv = asCsvF | asCsv = asCsvF
| asGeoJson = asGeoJsonF
| isJust binaryField && asXml = asXmlF $ fromJust binaryField | isJust binaryField && asXml = asXmlF $ fromJust binaryField
| isJust binaryField = asBinaryF $ fromJust binaryField | isJust binaryField = asBinaryF $ fromJust binaryField
| returnsSingle | returnsSingle
+1 -1
View File
@@ -431,7 +431,7 @@ requestContentTypes conf action path =
++ rawContentTypes conf ++ rawContentTypes conf
++ [CTOpenAPI | pIsRootSpec path] ++ [CTOpenAPI | pIsRootSpec path]
defaultContentTypes = defaultContentTypes =
[CTApplicationJSON, CTSingularJSON, CTTextCSV] [CTApplicationJSON, CTSingularJSON, CTGeoJSON, CTTextCSV]
rawContentTypes :: AppConfig -> [ContentType] rawContentTypes :: AppConfig -> [ContentType]
rawContentTypes AppConfig{..} = rawContentTypes AppConfig{..} =
+232
View File
@@ -0,0 +1,232 @@
module Feature.Query.PostGISSpec where
import Network.Wai (Application)
import Network.HTTP.Types
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import PostgREST.Config.PgVersion (PgVersion, pgVersion100)
import Protolude hiding (get)
import SpecHelper
spec :: PgVersion -> SpecWith ((), Application)
spec actualPgVersion = describe "PostGIS features" $
context "GeoJSON output" $ do
it "works for a table that has a geometry column" $
request methodGet "/shops"
[("Accept", "application/geo+json")] "" `shouldRespondWith`
[json| {
"type" : "FeatureCollection",
"features" : [
{"type": "Feature", "geometry": {"type":"Point","coordinates":[-71.10044,42.373695]}, "properties": {"id": 1, "address": "1369 Cambridge St"}}
, {"type": "Feature", "geometry": {"type":"Point","coordinates":[-71.10543,42.366432]}, "properties": {"id": 2, "address": "757 Massachusetts Ave"}}
, {"type": "Feature", "geometry": {"type":"Point","coordinates":[-71.081924,42.36437]}, "properties": {"id": 3, "address": "605 W Kendall St"}}
]} |]
{ matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] }
it "fails for a table that doesn't have a geometry column" $
request methodGet "/projects"
[("Accept", "application/geo+json")] "" `shouldRespondWith`
[json| {"hint":null,"details":null,"code":"22023","message":"geometry column is missing"} |]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
it "gives an empty features array on no rows" $
request methodGet "/shops?id=gt.3"
[("Accept", "application/geo+json")] "" `shouldRespondWith`
[json| {
"type" : "FeatureCollection",
"features" : []} |]
{ matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] }
it "must include the geometry column when using ?select" $
request methodGet "/shops?select=id,shop_geom&id=eq.1"
[("Accept", "application/geo+json")] "" `shouldRespondWith`
[json| {
"type" : "FeatureCollection",
"features" : [
{"type": "Feature", "geometry": {"type":"Point","coordinates":[-71.10044,42.373695]}, "properties": {"id": 1}}
] }|]
{ matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] }
it "works with resource embedding" $
request methodGet "/shops?select=*,shop_bles(*)&id=eq.1"
[("Accept", "application/geo+json")] "" `shouldRespondWith`
(if actualPgVersion >= pgVersion100
then [json| {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" },
"properties": {
"address": "1369 Cambridge St", "id": 1,
"shop_bles": [
{ "id": 1, "name": "Beacon-1", "shop_id": 1 ,
"coords": { "coordinates": [ -71.10044, 42.373695 ], "crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Point" },
"range_area": {
"coordinates": [ [ [ -71.10045254230499, 42.37387083326593 ], [ -71.10048070549963, 42.37377126199953 ], [ -71.10039688646793, 42.37375838212269 ], [ -71.10037006437777, 42.37385844878863 ], [ -71.10045254230499, 42.37387083326593 ] ] ],
"crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Polygon" }
},
{ "coords": { "coordinates": [ -71.10044, 42.373695 ], "crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Point" },
"id": 2, "name": "Beacon-2", "shop_id": 1,
"range_area": {
"coordinates": [ [ [ -71.10034391283989, 42.37385299961788 ], [ -71.10036939382553, 42.373756895982865 ], [ -71.1002916097641, 42.373745997623224 ], [ -71.1002641171217, 42.37384408279195 ], [ -71.10034391283989, 42.37385299961788 ] ] ],
"crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Polygon" }
}
]
}
}
]
}|]
else [json| {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" },
"properties": {
"address": "1369 Cambridge St", "id": 1,
"shop_bles": [
{ "id": 1, "name": "Beacon-1", "shop_id": 1 ,
"coords": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" },
"range_area": {
"coordinates": [ [ [ -71.10045254230499, 42.37387083326593 ], [ -71.10048070549963, 42.37377126199953 ], [ -71.10039688646793, 42.37375838212269 ], [ -71.10037006437777, 42.37385844878863 ], [ -71.10045254230499, 42.37387083326593 ] ] ],
"type": "Polygon" }
},
{ "coords": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" },
"id": 2, "name": "Beacon-2", "shop_id": 1,
"range_area": {
"coordinates": [ [ [ -71.10034391283989, 42.37385299961788 ], [ -71.10036939382553, 42.373756895982865 ], [ -71.1002916097641, 42.373745997623224 ], [ -71.1002641171217, 42.37384408279195 ], [ -71.10034391283989, 42.37385299961788 ] ] ],
"type": "Polygon" }
}
]
}
}
]
}|])
{ matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] }
it "works with RPC" $
request methodGet "/rpc/get_shop?id=1"
[("Accept", "application/geo+json")] "" `shouldRespondWith`
[json|{
"type" : "FeatureCollection",
"features" : [
{"type": "Feature", "geometry": {"type":"Point","coordinates":[-71.10044,42.373695]}, "properties": {"id": 1, "address": "1369 Cambridge St"} }
]
}|]
{ matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] }
it "works with Prefer: return=representation after POST" $
request methodPost "/shops"
[("Accept", "application/geo+json"), ("Prefer", "return=representation")] [json|
{"id": 4, "address": "1354 Massachusetts Ave", "shop_geom": "SRID=4326;POINT(-71.11834 42.373238)"}
|] `shouldRespondWith`
[json|{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "coordinates": [ -71.11834, 42.373238 ], "type": "Point" },
"properties": { "address": "1354 Massachusetts Ave", "id": 4 }
}
]
}|]
{ matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"]
}
it "works with Prefer: return=representation after PATCH" $
request methodPatch "/shops?id=eq.3"
[("Accept", "application/geo+json"), ("Prefer", "return=representation")]
[json| { "address": "1354 Massachusetts Avenue"} |]
`shouldRespondWith`
[json|{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "coordinates": [-71.081924,42.36437], "type": "Point" },
"properties": { "address": "1354 Massachusetts Avenue", "id": 3 }
}
]
}|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"]
}
it "works with Prefer: return=representation after PUT" $
request methodPut "/shops?id=eq.3"
[("Accept", "application/geo+json"), ("Prefer", "return=representation")]
[json| { "id": 3, "address": "1354 Massachusetts Avenue"} |]
`shouldRespondWith`
[json|{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "coordinates": [-71.081924,42.36437], "type": "Point" },
"properties": { "address": "1354 Massachusetts Avenue", "id": 3 }
}
]
}|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"]
}
it "works with Prefer: return=representation after DELETE" $
request methodDelete "/shops?id=eq.3"
[("Accept", "application/geo+json"), ("Prefer", "return=representation")] "" `shouldRespondWith`
[json|{
"type" : "FeatureCollection",
"features" : [
{"type": "Feature", "geometry": {"type":"Point","coordinates":[-71.081924,42.36437]}, "properties": {"id": 3, "address": "605 W Kendall St"}}
]
}|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"]
}
context "multiple geometry columns" $
it "can select the geometry column to get as a feature with ?select" $ do
request methodGet "/shop_bles?select=id,name,coords&id=eq.1"
[("Accept", "application/geo+json")] "" `shouldRespondWith`
[json|{
"type" : "FeatureCollection",
"features" : [
{"type": "Feature",
"geometry": {"type":"Point","coordinates":[-71.10044,42.373695]},
"properties": {"id": 1, "name": "Beacon-1"}
}
]}|]
{ matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] }
request methodGet "/shop_bles?select=id,name,range_area&id=eq.1"
[("Accept", "application/geo+json")] "" `shouldRespondWith`
[json|{
"type" : "FeatureCollection",
"features" : [
{"type": "Feature",
"geometry": {"type":"Polygon","coordinates":[[[-71.100452542,42.373870833],[-71.100480705,42.373771262],[-71.100396886,42.373758382],[-71.100370064,42.373858449],[-71.100452542,42.373870833]]]},
"properties": {"id": 1, "name": "Beacon-1"}}
]}|]
{ matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] }
it "gets the geojson geometry object with the regular application/json output" $
request methodGet "/shops?id=eq.1" [] "" `shouldRespondWith`
(if actualPgVersion >= pgVersion100
then [json|[{
"id":1,"address":"1369 Cambridge St",
"shop_geom":{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"coordinates":[-71.10044,42.373695]}
}]|]
else [json|[{
"address": "1369 Cambridge St",
"id": 1,
"shop_geom": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" }
}]|])
{ matchHeaders = [matchContentTypeJson] }
+3 -1
View File
@@ -44,6 +44,7 @@ import qualified Feature.Query.HtmlRawOutputSpec
import qualified Feature.Query.InsertSpec import qualified Feature.Query.InsertSpec
import qualified Feature.Query.JsonOperatorSpec import qualified Feature.Query.JsonOperatorSpec
import qualified Feature.Query.MultipleSchemaSpec import qualified Feature.Query.MultipleSchemaSpec
import qualified Feature.Query.PostGISSpec
import qualified Feature.Query.QueryLimitedSpec import qualified Feature.Query.QueryLimitedSpec
import qualified Feature.Query.QuerySpec import qualified Feature.Query.QuerySpec
import qualified Feature.Query.RangeSpec import qualified Feature.Query.RangeSpec
@@ -199,8 +200,9 @@ main = do
describe "Feature.Query.ErrorSpec" Feature.Query.ErrorSpec.spec describe "Feature.Query.ErrorSpec" Feature.Query.ErrorSpec.spec
-- this test runs with an extra search path -- this test runs with an extra search path
parallel $ before extraSearchPathApp $ parallel $ before extraSearchPathApp $ do
describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec
describe "Feature.Query.PostGISSpec" $ Feature.Query.PostGISSpec.spec actualPgVersion
-- this test runs with a root spec function override -- this test runs with a root spec function override
parallel $ before rootSpecApp $ parallel $ before rootSpecApp $
+11
View File
@@ -784,3 +784,14 @@ INSERT INTO test.bulk_update_items (id, name, observation) VALUES (1, 'item-1',
TRUNCATE TABLE test.bulk_update_items_cpk CASCADE; TRUNCATE TABLE test.bulk_update_items_cpk CASCADE;
INSERT INTO test.bulk_update_items_cpk (id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL); INSERT INTO test.bulk_update_items_cpk (id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);
TRUNCATE TABLE shops CASCADE;
INSERT INTO shops(id, address, shop_geom) VALUES(1, '1369 Cambridge St', 'SRID=4326;POINT(-71.10044 42.373695)');
INSERT INTO shops(id, address, shop_geom) VALUES(2, '757 Massachusetts Ave', 'SRID=4326;POINT(-71.10543 42.366432)');
INSERT INTO shops(id, address, shop_geom) VALUES(3, '605 W Kendall St', 'SRID=4326;POINT(-71.081924 42.36437)');
TRUNCATE TABLE shop_bles CASCADE;
INSERT INTO shop_bles(id, name, coords, shop_id, range_area) VALUES(1, 'Beacon-1', 'SRID=4326;POINT(-71.10044 42.373695)', 1,
extensions.ST_GeomFromGeoJSON('{"type": "Polygon", "coordinates": [ [ [ -71.10045254230499, 42.37387083326593 ], [ -71.10048070549963, 42.37377126199953 ], [ -71.10039688646793, 42.37375838212269 ], [ -71.10037006437777, 42.37385844878863 ], [ -71.10045254230499, 42.37387083326593 ] ] ]}'));
INSERT INTO shop_bles(id, name, coords, shop_id, range_area) VALUES(2, 'Beacon-2', 'SRID=4326;POINT(-71.10044 42.373695)', 1,
extensions.ST_GeomFromGeoJSON('{"type": "Polygon", "coordinates": [ [ [ -71.10034391283989, 42.37385299961788 ], [ -71.10036939382553, 42.373756895982865 ], [ -71.1002916097641, 42.373745997623224 ], [ -71.1002641171217, 42.37384408279195 ], [ -71.10034391283989, 42.37385299961788 ] ] ]}'));
+2
View File
@@ -188,6 +188,8 @@ GRANT ALL ON TABLE
, view_test , view_test
, bulk_update_items , bulk_update_items
, bulk_update_items_cpk , bulk_update_items_cpk
, shops
, shop_bles
TO postgrest_test_anonymous; TO postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous; GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
+20
View File
@@ -2649,3 +2649,23 @@ CREATE TABLE test.bulk_update_items_cpk (
observation TEXT, observation TEXT,
PRIMARY KEY (id, name) PRIMARY KEY (id, name)
); );
create extension if not exists postgis with schema extensions;
create table shops (
id int primary key
, address text
, shop_geom extensions.geometry(POINT, 4326)
);
create table shop_bles (
id int primary key
, name text
, coords extensions.geometry(POINT, 4326)
, range_area extensions.geometry(POLYGON, 4326)
, shop_id int references shops(id)
);
create function get_shop(id int) returns shops as $$
select * from shops where id = $1;
$$ language sql;