mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
Closes [DOCS-1289](https://linear.app/supabase/issue/DOCS-1289/get-the-linter-to-fix-what-it-flags-or-retirereplace-the-linter) Stacked on #50600, which points contributors at the authoring skills. Merge that one first. ## Problem Contributors experienced friction with the linter. They felt nickle and dimed for tiny nits and felt detracted from the work itself. PRs would become noisy with tiny one-word suggestions. Additionally, our homegrown linter is not very intelligent, causing frequent overrides. ## Solution This removes the linter entirely in favor of directing contributors to use SKILLS instead. The removal entails... - **CI.** Delete the three `docs_lint` workflows: the PR check, the external-PR comment companion, and the nightly `--fix` bot. Drop the stale `zizmor.yml` ignore entry for the deleted workflow. - **Tooling.** Delete `supa-mdx-lint.config.toml` and the 14 rule files. Drop the `lint:mdx` script and the `@supabase/supa-mdx-lint` dependency from docs, learn, and ui-library, and regenerate the lockfile. - **Content.** Remove the 181 directives. A separate commit carries Prettier's reformatting of the tables and blank lines those comments had suppressed, so the deletion commit stays readable. No prose changes. - **Style guide.** The word list states each rule directly instead of describing what the linter flagged. Every term survives, including the phrase groups that mirrored `Rule004ExcludeWords`. - **Skills.** `write-the-docs`, `edit-the-docs`, and `review-the-docs` drop `pnpm lint:mdx` from their self-review commands and check the word list directly. `ask-the-docs`'s CI reference drops both workflows. ## Manual testing 1. Run `git grep -i supa-mdx-lint -- . ':!pnpm-lock.yaml'`. No matches. 2. Run `pnpm install --frozen-lockfile --lockfile-only`. It passes, so the lockfile matches the three trimmed manifests. 3. Run `git diff master...HEAD --name-only --diff-filter=ACMR | grep -E '\.(md|mdx)$' | xargs npx prettier --config prettier.config.mjs --check`. All changed markdown passes. 4. Open the [reformatted filter table](https://docs-git-docs-retire-mdx-linter-supabase.vercel.app/docs/guides/observability/logs#filter-events) on the preview and compare it with [production](https://supabase.com/docs/guides/observability/logs#filter-events). The table renders the same. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Documentation guidance now uses manual prose and terminology review with the shared word list. * Clarified storage configuration and common Realtime channel mistakes. * Improved table formatting, text wrapping, and selected reference links. * Updated documentation authoring and review guidance. * **Chores** * Retired automated MDX linting from workflows and local validation commands. * Removed lint-suppression markers throughout documentation without changing instructions. * Added targeted documentation review guidance for pull requests. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
552 lines
16 KiB
Plaintext
552 lines
16 KiB
Plaintext
---
|
|
id: 'postgis'
|
|
title: 'PostGIS: Geo queries'
|
|
description: 'Working with geo-spatial data in Postgres'
|
|
tocVideo: 'agFsGDJxjwA'
|
|
---
|
|
|
|
[PostGIS](https://postgis.net/) is a Postgres extension that allows you to interact with Geo data within Postgres. You can sort your data by geographic location, get data within certain geographic boundaries, and do much more with it.
|
|
|
|
## Overview
|
|
|
|
While you may be able to store lat/long geographic coordinates as a set of decimals, it does not scale very well when you try to query through a large data set. PostGIS comes with special data types that are efficient, and indexable for high scalability.
|
|
|
|
The additional data types that PostGIS provides include [Point](https://postgis.net/docs/using_postgis_dbmanagement.html#Point), [Polygon](https://postgis.net/docs/using_postgis_dbmanagement.html#Polygon), [LineString](https://postgis.net/docs/using_postgis_dbmanagement.html#LineString), and many more to represent different types of geographical data. In this guide, we will mainly focus on how to interact with `Point` type, which represents a single set of latitude and longitude. If you are interested in digging deeper, you can learn more about different data types on the [data management section of PostGIS docs](https://postgis.net/docs/using_postgis_dbmanagement.html).
|
|
|
|
## Enable the extension
|
|
|
|
You can get started with PostGIS by enabling the PostGIS extension in your Supabase dashboard.
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="dashboard"
|
|
queryGroup="database-method"
|
|
>
|
|
<TabPanel id="dashboard" label="Dashboard">
|
|
|
|
1. Go to the [Database](/dashboard/project/_/database/tables) page in the Dashboard.
|
|
2. Click on **Extensions** in the sidebar.
|
|
3. Search for `postgis` and enable the extension.
|
|
4. In the confirmation prompt select "Create a new schema" and name it `gis` for example.
|
|
|
|
</TabPanel>
|
|
<TabPanel id="sql" label="SQL">
|
|
|
|
```sql
|
|
-- Example: enable the "postgis" extension
|
|
create extension postgis with schema "extensions";
|
|
|
|
-- Example: disable the "postgis" extension
|
|
drop extension if exists postgis;
|
|
```
|
|
|
|
</TabPanel>
|
|
</Tabs>
|
|
|
|
<Admonition type="caution">
|
|
|
|
Always install PostGIS into a dedicated schema (`extensions` in the examples above), never `public`. PostGIS creates the `spatial_ref_sys` reference table in whichever schema you install into, and if that's `public`, the table is exposed through the Data API — see [Troubleshooting](#troubleshooting) if this has already happened to your project.
|
|
|
|
</Admonition>
|
|
|
|
## Examples
|
|
|
|
To get started with PostGIS, create a table and see to use PostGIS for some typical use cases. Imagine creating a basic restaurant-searching app.
|
|
|
|
Create the table. Each row represents a restaurant with its location stored in `location` column as a `Point` type.
|
|
|
|
```sql
|
|
create table if not exists public.restaurants (
|
|
id int generated by default as identity primary key,
|
|
name text not null,
|
|
location extensions.geography(POINT) not null
|
|
);
|
|
```
|
|
|
|
We can then set a [spatial index](https://postgis.net/docs/using_postgis_dbmanagement.html#build-indexes) on the `location` column of this table.
|
|
|
|
```sql
|
|
create index restaurants_geo_index
|
|
on public.restaurants
|
|
using GIST (location);
|
|
```
|
|
|
|
### Inserting data
|
|
|
|
You can insert geographical data through SQL or through our API.
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="data"
|
|
queryGroup="language"
|
|
>
|
|
<TabPanel id="data" label="Data">
|
|
|
|
<h4>Restaurants</h4>
|
|
|
|
| id | name | location |
|
|
| --- | ----------- | -------------------------------- |
|
|
| 1 | Supa Burger | lat: 40.807416, long: -73.946823 |
|
|
| 2 | Supa Pizza | lat: 40.807475, long: -73.94581 |
|
|
| 3 | Supa Taco | lat: 40.80629, long: -73.945826 |
|
|
|
|
</TabPanel>
|
|
<TabPanel id="sql" label="SQL">
|
|
|
|
```sql
|
|
insert into public.restaurants
|
|
(name, location)
|
|
values
|
|
('Supa Burger', extensions.st_point(-73.946823, 40.807416)),
|
|
('Supa Pizza', extensions.st_point(-73.94581, 40.807475)),
|
|
('Supa Taco', extensions.st_point(-73.945826, 40.80629));
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="js" label="JavaScript">
|
|
|
|
```js
|
|
const { error } = await supabase.from('restaurants').insert([
|
|
{
|
|
name: 'Supa Burger',
|
|
location: 'POINT(-73.946823 40.807416)',
|
|
},
|
|
{
|
|
name: 'Supa Pizza',
|
|
location: 'POINT(-73.94581 40.807475)',
|
|
},
|
|
{
|
|
name: 'Supa Taco',
|
|
location: 'POINT(-73.945826 40.80629)',
|
|
},
|
|
])
|
|
```
|
|
|
|
</TabPanel>
|
|
<$Show if="sdk:dart">
|
|
<TabPanel id="dart" label="Dart">
|
|
|
|
```dart
|
|
await supabase.from('restaurants').insert([
|
|
{
|
|
'name': 'Supa Burger',
|
|
'location': 'POINT(-73.946823 40.807416)',
|
|
},
|
|
{
|
|
'name': 'Supa Pizza',
|
|
'location': 'POINT(-73.94581 40.807475)',
|
|
},
|
|
{
|
|
'name': 'Supa Taco',
|
|
'location': 'POINT(-73.945826 40.80629)',
|
|
},
|
|
]);
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:swift">
|
|
<TabPanel id="swift" label="Swift">
|
|
|
|
```swift
|
|
struct Restaurant: Codable {
|
|
let name: String
|
|
let location: String // You could also use a custom type with a custom `Encodable` conformance for convenience.
|
|
}
|
|
|
|
try await supabase.from("restaurants")
|
|
.insert(
|
|
[
|
|
Restaurant(name: "Supa Burger", location: "POINT(-73.946823 40.807416)"),
|
|
Restaurant(name: "Supa Pizza", location: "POINT(-73.94581 40.807475)"),
|
|
Restaurant(name: "Supa Taco", location: "POINT(-73.945826 40.80629)"),
|
|
]
|
|
)
|
|
.execute()
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:kotlin">
|
|
<TabPanel id="kotlin" label="Kotlin">
|
|
|
|
```kotlin
|
|
@Serializable
|
|
data class Restaurant(
|
|
val name: String,
|
|
val location: String //you could also use a custom type with a custom serializer for more type safety
|
|
)
|
|
```
|
|
|
|
```kotlin
|
|
val data = supabase.from("restaurants").insert(listOf(
|
|
Restaurant("Supa Burger", "POINT(-73.946823 40.807416)"),
|
|
Restaurant("Supa Pizza", "POINT(-73.94581 40.807475)"),
|
|
Restaurant("Supa Taco", "POINT(-73.945826 40.80629)"),
|
|
))
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:csharp">
|
|
<TabPanel id="csharp" label="C#">
|
|
|
|
```c#
|
|
[Table("restaurants")]
|
|
class Restaurant : BaseModel
|
|
{
|
|
[PrimaryKey("id", false)]
|
|
public long Id { get; set; }
|
|
|
|
[Column("name")]
|
|
public string Name { get; set; }
|
|
|
|
[Column("location")]
|
|
public string Location { get; set; }
|
|
}
|
|
```
|
|
|
|
```c#
|
|
var restaurants = new List<Restaurant>
|
|
{
|
|
new Restaurant { Name = "Supa Burger", Location = "POINT(-73.946823 40.807416)" },
|
|
new Restaurant { Name = "Supa Pizza", Location = "POINT(-73.94581 40.807475)" },
|
|
new Restaurant { Name = "Supa Taco", Location = "POINT(-73.945826 40.80629)" }
|
|
};
|
|
|
|
await supabase.From<Restaurant>().Insert(restaurants);
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
</Tabs>
|
|
|
|
Notice the order in which you pass the latitude and longitude. Longitude comes first, and is because longitude represents the x-axis of the location. Another thing to watch for is when inserting data from the client library, there is no comma between the two values, only a single space.
|
|
|
|
At this point, if you go into your Supabase dashboard and look at the data, you will notice that the value of the `location` column looks something like this.
|
|
|
|
```
|
|
0101000020E6100000A4DFBE0E9C91614044FAEDEBC0494240
|
|
```
|
|
|
|
We can query the `restaurants` table directly, but it will return the `location` column in the format you see above.
|
|
We will create [database functions](/docs/guides/database/functions) so that we can use the [st_y()](https://postgis.net/docs/ST_Y.html) and [st_x()](https://postgis.net/docs/ST_X.html) function to convert it back to lat and long floating values.
|
|
|
|
### Order by distance
|
|
|
|
Sorting datasets from closest to farthest, sometimes called nearest-neighbor sort, is a very common use case in Geo-queries. PostGIS can handle it with the use of the [`<->`](https://postgis.net/docs/geometry_distance_knn.html) operator. `<->` operator returns the two-dimensional distance between two geometries and uses the spatial index when used within `order by` clause. You can create the following database function to sort the restaurants from closest to farthest by passing the current locations as parameters.
|
|
|
|
```sql
|
|
create or replace function nearby_restaurants(lat float, long float)
|
|
returns table (id public.restaurants.id%TYPE, name public.restaurants.name%TYPE, lat float, long float, dist_meters float)
|
|
set search_path = ''
|
|
language sql
|
|
as $$
|
|
select id, name, extensions.st_y(location::extensions.geometry) as lat, extensions.st_x(location::extensions.geometry) as long, extensions.st_distance(location, extensions.st_point(long, lat)::extensions.geography) as dist_meters
|
|
from public.restaurants
|
|
order by location operator(extensions.<->) extensions.st_point(long, lat)::extensions.geography;
|
|
$$;
|
|
```
|
|
|
|
Now you can call this function from your client using `rpc()` like this:
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="js"
|
|
queryGroup="language"
|
|
>
|
|
<TabPanel id="js" label="JavaScript">
|
|
|
|
```js
|
|
const { data, error } = await supabase.rpc('nearby_restaurants', {
|
|
lat: 40.807313,
|
|
long: -73.946713,
|
|
})
|
|
```
|
|
|
|
</TabPanel>
|
|
<$Show if="sdk:dart">
|
|
<TabPanel id="dart" label="Dart">
|
|
|
|
```dart
|
|
final data = await supabase.rpc('nearby_restaurants',params: {
|
|
'lat': 40.807313,
|
|
'long': -73.946713,
|
|
});
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:swift">
|
|
<TabPanel id="swift" label="Swift">
|
|
|
|
```swift
|
|
struct Response: Codable {
|
|
let id: Int
|
|
let name: String
|
|
let lat: Double
|
|
let long: Double
|
|
let distance: Double
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, name, lat, long
|
|
case distance = "dist_meters"
|
|
}
|
|
}
|
|
|
|
let response: Response = try await supabase.rpc(
|
|
"nearby_restaurants",
|
|
params: [
|
|
"lat": 40.807313,
|
|
"long": -73.946713
|
|
]
|
|
)
|
|
.execute()
|
|
.value
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:kotlin">
|
|
<TabPanel id="kotlin" label="Kotlin">
|
|
|
|
```kotlin
|
|
val data = supabase.postgrest.rpc(
|
|
function = "nearby_restaurants",
|
|
parameters = buildJsonObject { //You can put here any serializable object including your own classes
|
|
put("lat", 40.807313)
|
|
put("lon", -73.946713)
|
|
}
|
|
)
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:csharp">
|
|
<TabPanel id="csharp" label="C#">
|
|
|
|
```c#
|
|
await supabase.Rpc("nearby_restaurants", new Dictionary<string, object>
|
|
{
|
|
{ "lat", 40.807313 },
|
|
{ "long", -73.946713 }
|
|
});
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<TabPanel id="result" label="Result">
|
|
|
|
```json
|
|
[
|
|
{
|
|
"id": 1,
|
|
"name": "Supa Burger",
|
|
"lat": 40.807416,
|
|
"long": -73.946823,
|
|
"dist_meters": 14.73033739
|
|
},
|
|
{
|
|
"id": 2,
|
|
"name": "Supa Pizza",
|
|
"lat": 40.807475,
|
|
"long": -73.94581,
|
|
"dist_meters": 78.28980007
|
|
},
|
|
{
|
|
"id": 3,
|
|
"name": "Supa Taco",
|
|
"lat": 40.80629,
|
|
"long": -73.945826,
|
|
"dist_meters": 136.04329002
|
|
}
|
|
]
|
|
```
|
|
|
|
</TabPanel>
|
|
</Tabs>
|
|
|
|
### Finding all data points within a bounding box
|
|
|
|

|
|
|
|
When you are working on a map-based application where the user scrolls through your map, you might want to load the data that lies within the bounding box of the map every time your users scroll. PostGIS can return the rows that are within the bounding box by supplying the bottom left and the top right coordinates. The function looks like this:
|
|
|
|
```sql
|
|
create or replace function restaurants_in_view(min_lat float, min_long float, max_lat float, max_long float)
|
|
returns table (id public.restaurants.id%TYPE, name public.restaurants.name%TYPE, lat float, long float)
|
|
set search_path to ''
|
|
language sql
|
|
as $$
|
|
select id, name, extensions.st_y(location::extensions.geometry) as lat, extensions.st_x(location::extensions.geometry) as long
|
|
from public.restaurants
|
|
where location operator(extensions.&&) extensions.ST_SetSRID(extensions.ST_MakeBox2D(extensions.ST_Point(min_long, min_lat), extensions.ST_Point(max_long, max_lat)), 4326)
|
|
$$;
|
|
```
|
|
|
|
The [`&&`](https://postgis.net/docs/geometry_overlaps.html) operator used in the `where` statement here returns a boolean of whether the bounding box of the two geometries intersect or not. It creates a bounding box from the two points and finds those points that fall under the bounding box. It also uses a few PostGIS functions:
|
|
|
|
- [ST_MakeBox2D](https://postgis.net/docs/ST_MakeBox2D.html): Creates a 2-dimensional box from two points.
|
|
- [ST_SetSRID](https://postgis.net/docs/ST_SetSRID.html): Sets the [SRID](https://postgis.net/docs/manual-dev/using_postgis_dbmanagement.html#spatial_ref_sys), which is an identifier of what coordinate system to use for the geometry. 4326 is the standard longitude and latitude coordinate system.
|
|
|
|
You can call this function from your client using `rpc()` like this:
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="js"
|
|
queryGroup="language"
|
|
>
|
|
<TabPanel id="js" label="JavaScript">
|
|
|
|
```js
|
|
const { data, error } = await supabase.rpc('restaurants_in_view', {
|
|
min_lat: 40.807,
|
|
min_long: -73.946,
|
|
max_lat: 40.808,
|
|
max_long: -73.945,
|
|
})
|
|
```
|
|
|
|
</TabPanel>
|
|
<$Show if="sdk:dart">
|
|
<TabPanel id="dart" label="Dart">
|
|
|
|
```dart
|
|
final data = await supabase.rpc('restaurants_in_view', params: {
|
|
'min_lat': 40.807,
|
|
'min_long': -73.946,
|
|
'max_lat': 40.808,
|
|
'max_long': -73.945,
|
|
});
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:swift">
|
|
<TabPanel id="swift" label="Swift">
|
|
|
|
```swift
|
|
struct Response: Codable {
|
|
let id: Int
|
|
let name: String
|
|
let lat: Double
|
|
let long: Double
|
|
}
|
|
|
|
let response: Response = try await supabase.rpc(
|
|
"restaurants_in_view",
|
|
params: [
|
|
"min_lat": 40.807,
|
|
"min_long": -73.946,
|
|
"max_long": -73.945,
|
|
"max_lat": 40.808,
|
|
]
|
|
)
|
|
.execute()
|
|
.value
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:kotlin">
|
|
<TabPanel id="kotlin" label="Kotlin">
|
|
|
|
```kotlin
|
|
val data = supabase.postgrest.rpc(
|
|
function = "restaurants_in_view",
|
|
parameters = buildJsonObject { //You can put here any serializable object including your own classes
|
|
put("min_lat", 40.807)
|
|
put("min_lon", -73.946)
|
|
put("max_lat", 40.808)
|
|
put("max_lon", -73.945)
|
|
}
|
|
)
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:csharp">
|
|
<TabPanel id="csharp" label="C#">
|
|
|
|
```c#
|
|
await supabase.Rpc("restaurants_in_view", new Dictionary<string, object>
|
|
{
|
|
{ "min_lat", 40.807 },
|
|
{ "min_long", -73.946 },
|
|
{ "max_lat", 40.808 },
|
|
{ "max_long", -73.945 }
|
|
});
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<TabPanel id="result" label="Result">
|
|
|
|
```json
|
|
[
|
|
{
|
|
"id": 2,
|
|
"name": "Supa Pizza",
|
|
"lat": 40.807475,
|
|
"long": -73.94581
|
|
}
|
|
]
|
|
```
|
|
|
|
</TabPanel>
|
|
</Tabs>
|
|
|
|
## Troubleshooting
|
|
|
|
### Security advisor flags `public.spatial_ref_sys`
|
|
|
|
If `PostGIS` was installed in the `public` schema, the [Security Advisor](/dashboard/project/_/advisors/security) may report that `public.spatial_ref_sys` grants write access to the Data API roles (`anon`, `authenticated`), and that it can't be fixed by enabling RLS because your project's `postgres` role doesn't own the table.
|
|
|
|
This is expected, not a data exposure risk: `spatial_ref_sys` is `PostGIS`'s built-in lookup table of coordinate system definitions, and it never holds your data. The actual problem is that `PostGIS` is installed in `public`, a schema the Data API exposes by default. Enabling RLS on the table isn't possible for your `postgres` role, and it isn't the fix. Follow the steps below to move `PostGIS` out of `public` yourself. They include a backup, since the default path drops and recreates the extension. To avoid the rebuild, contact Supabase Support instead.
|
|
|
|
### Moving `PostGIS` to a different schema
|
|
|
|
As of PostGIS 2.3 or newer, the PostGIS extension is no longer relocatable from one schema to another. If you need to move it from one schema to another for any reason (e.g. from the public schema to the extensions schema for security reasons), you would normally run a ALTER EXTENSION to relocate the schema. However, you will now to do the following steps:
|
|
|
|
1. Backup your Database to prevent data loss - You can do this through the [CLI](/docs/reference/cli/supabase-db-dump) or Postgres backup tools such as [pg_dumpall](https://www.postgresql.org/docs/current/backup-dump.html#BACKUP-DUMP-ALL)
|
|
|
|
2. Drop all dependencies you created and the PostGIS extension - `DROP EXTENSION postgis CASCADE;`
|
|
|
|
3. Enable PostGIS extension in the new schema - `CREATE EXTENSION postgis SCHEMA extensions;`
|
|
|
|
4. Restore dropped data via the Backup if necessary from step 1 with your tool of choice.
|
|
|
|
Alternatively, you can contact the [Supabase Support Team](/dashboard/support/new) and ask them to run the following SQL on your instance:
|
|
|
|
```sql
|
|
BEGIN;
|
|
UPDATE pg_extension
|
|
SET extrelocatable = true
|
|
WHERE extname = 'postgis';
|
|
|
|
ALTER EXTENSION postgis
|
|
SET SCHEMA extensions;
|
|
|
|
ALTER EXTENSION postgis
|
|
UPDATE TO "<POSTGIS_VERSION>next";
|
|
|
|
ALTER EXTENSION postgis UPDATE;
|
|
|
|
UPDATE pg_extension
|
|
SET extrelocatable = false
|
|
WHERE extname = 'postgis';
|
|
COMMIT;
|
|
```
|
|
|
|
## Resources
|
|
|
|
- [Official PostGIS documentation](https://postgis.net/documentation/)
|