Skip to main content
Version: Latest - 1.0.0-alpha.25

PostGIS

PostGIS adds support for geographic objects to PostgreSQL. Durcno provides type-safe column types for PostGIS geography types, with automatic serialization to/from GeoJSON coordinate arrays.

Prerequisites

Enable the PostGIS extension in your database:

CREATE EXTENSION IF NOT EXISTS postgis;

Import

All PostGIS column types are available through the geography namespace:

import { geography } from "durcno";

Geography Column Types

All geography columns accept these common options:

OptionTypeDefaultDescription
sridnumber4326Spatial Reference System Identifier (WGS 84 by default)
notNullsymbolMakes the column NOT NULL
uniquesymbolAdds a UNIQUE constraint

geography.point

Represents a single geographic point (longitude, latitude).

  • SQL type: geography(POINT, <srid>)
  • JS type: [number, number][longitude, latitude]
import { table, pk, varchar, notNull, geography } from "durcno";

export const Locations = table("public", "locations", {
id: pk(),
name: varchar({ length: 255, notNull }),
coordinates: geography.point({ notNull }),
});

// Insert a point
await db.insert(Locations).values({
name: "Eiffel Tower",
coordinates: [2.2945, 48.8584], // [longitude, latitude]
});

// Select returns the same tuple format
const locations = await db.from(Locations).select();
// locations[0].coordinates → [2.2945, 48.8584]

geography.multipoint

Represents a collection of points.

  • SQL type: geography(MULTIPOINT, <srid>)
  • JS type: [number, number][] — array of [longitude, latitude] pairs
export const Routes = table("public", "routes", {
id: pk(),
waypoints: geography.multipoint({ notNull }),
});

await db.insert(Routes).values({
waypoints: [
[2.2945, 48.8584],
[2.3522, 48.8566],
[2.3376, 48.8606],
],
});

geography.linestring

Represents a connected sequence of points forming a line.

  • SQL type: geography(LINESTRING, <srid>)
  • JS type: [number, number][] — array of [longitude, latitude] pairs
export const Trails = table("public", "trails", {
id: pk(),
name: varchar({ length: 255, notNull }),
path: geography.linestring({ notNull }),
});

await db.insert(Trails).values({
name: "River Walk",
path: [
[-73.9857, 40.7484],
[-73.9851, 40.748],
[-73.9845, 40.7478],
],
});

geography.multilinestring

Represents a collection of linestrings.

  • SQL type: geography(MULTILINESTRING, <srid>)
  • JS type: [number, number][][] — array of linestrings
export const TransitRoutes = table("public", "transitRoutes", {
id: pk(),
name: varchar({ length: 255, notNull }),
lines: geography.multilinestring({ notNull }),
});

await db.insert(TransitRoutes).values({
name: "Bus Route 42",
lines: [
// First line segment
[
[-73.9857, 40.7484],
[-73.9851, 40.748],
],
// Second line segment
[
[-73.984, 40.7475],
[-73.9835, 40.747],
],
],
});

geography.polygon

Represents a polygon with an exterior ring and optional interior rings (holes).

  • SQL type: geography(POLYGON, <srid>)
  • JS type: [number, number][][] — array of rings, where each ring is an array of [longitude, latitude] pairs. The first ring is the exterior boundary; subsequent rings are holes.
export const Zones = table("public", "zones", {
id: pk(),
name: varchar({ length: 255, notNull }),
boundary: geography.polygon({ notNull }),
});

await db.insert(Zones).values({
name: "Central Park",
boundary: [
// Exterior ring (must be closed — first and last points match)
[
[-73.9819, 40.7681],
[-73.958, 40.8006],
[-73.9498, 40.7968],
[-73.9737, 40.7644],
[-73.9819, 40.7681],
],
],
});

geography.multipolygon

Represents a collection of polygons.

  • SQL type: geography(MULTIPOLYGON, <srid>)
  • JS type: [number, number][][][] — array of polygons
export const Districts = table("public", "districts", {
id: pk(),
name: varchar({ length: 255, notNull }),
areas: geography.multipolygon({ notNull }),
});

await db.insert(Districts).values({
name: "Borough Parks",
areas: [
// First polygon
[
[
[-73.97, 40.77],
[-73.96, 40.78],
[-73.95, 40.77],
[-73.97, 40.77],
],
],
// Second polygon
[
[
[-73.99, 40.75],
[-73.98, 40.76],
[-73.97, 40.75],
[-73.99, 40.75],
],
],
],
});

Custom SRID

By default all geography columns use SRID 4326 (WGS 84, the standard for GPS coordinates). You can specify a different SRID:

export const LocalPoints = table("public", "localPoints", {
id: pk(),
position: geography.point({ srid: 3857, notNull }), // Web Mercator
});

Data Format

Durcno uses GeoJSON coordinate ordering ([longitude, latitude]) for all geography types. Data is automatically converted between:

  • TypeScript ↔ GeoJSON coordinate arrays (what you read/write)
  • PostgreSQL ↔ EWKT with SRID (the internal wire format)

This means you can work with familiar [lng, lat] tuples in your application code while Durcno handles the database serialization transparently.

Spatial Filters

Durcno provides type-safe spatial filter functions for use in .where() clauses. All filters accept a geography column and a reference coordinate typed as the column's ValType — so passing the wrong column type is a compile-time error.

import {
stContains,
stDistance,
stDWithin,
stIntersects,
stWithin,
} from "durcno";

Boolean Predicates

These functions return a boolean condition usable in .where():

FunctionSQL generatedDescription
stDWithin(col, point, radius, srid?)ST_DWithin(col, ST_SetSRID(ST_MakePoint(...), srid), radius)True if column is within radius metres of point
stIntersects(col, point, srid?)ST_Intersects(col, ST_SetSRID(ST_MakePoint(...), srid))True if column intersects point
stContains(col, point, srid?)ST_Contains(col, ST_SetSRID(ST_MakePoint(...), srid))True if column contains point
stWithin(col, point, srid?)ST_Within(col, ST_SetSRID(ST_MakePoint(...), srid))True if column is within point geometry

The point argument is typed as Col["ValType"] — inferred directly from the column definition.

const Properties = table("public", "properties", {
id: pk(),
type: varchar({ length: 50, notNull }),
location: geography.point({ notNull }),
availableFrom: timestamp({ notNull }),
});

// Find properties near a given location
const nearby = await db
.from(Properties)
.select()
.where(
and(
stDWithin(Properties.location, [centerLon, centerLat], input.radius),
eq(Properties.type, input.type),
lte(Properties.availableFrom, new Date(input.date)),
),
);

stDistance — Value Expression

stDistance is a typed SQL value expression (not a filter) that computes the distance in metres between a geography column and a given point. It can be used in three contexts:

const dist = stDistance(Properties.location, [centerLon, centerLat]);

// 1. In select — adds a computed numeric column to the result
const rows = await db
.from(Properties)
.select({ id: Properties.id, distance: dist });
// rows[0].distance → number (metres)

// 2. In orderBy — order results by proximity
const byProximity = await db.from(Properties).select().orderBy(asc(dist));

// 3. In where via comparison operators
const withinRange = await db.from(Properties).select().where(lt(dist, 5000)); // closer than 5 km

All three can be combined:

const dist = stDistance(Properties.location, [centerLon, centerLat]);

const results = await db
.from(Properties)
.select({ id: Properties.id, type: Properties.type, distance: dist })
.orderBy(asc(dist))
.where(and(lt(dist, input.radius), eq(Properties.type, input.type)));
// results[0] → { id: bigint; type: string; distance: number }

Type Safety

All spatial filter functions are constrained to geography point columns only. Passing a non-geography column is a compile-time error:

// ✅ Valid — geography.point column
stDWithin(Properties.location, [2.29, 48.85], 5000);

// ❌ Compile error — varchar is not a geography column
stDWithin(Properties.type, [2.29, 48.85], 5000);