From f7226d1191e75c3f6c583f862e52e1296cd805dd Mon Sep 17 00:00:00 2001 From: tangoyankee Date: Mon, 8 Jul 2024 16:41:43 -0400 Subject: [PATCH] try to run migration from introspection --- drizzle/api.config.ts | 1 + drizzle/drizzle-pgis/geography.ts | 41 +++++ drizzle/drizzle-pgis/geometry.ts | 41 +++++ drizzle/drizzle-pgis/index.ts | 25 +++ drizzle/drizzle-pgis/readme.md | 24 +++ drizzle/drizzle-pgis/spatial-type.ts | 23 +++ drizzle/drizzle-pgis/types.ts | 9 ++ drizzle/flow.config.ts | 18 +++ ...tarjammers.sql => 0000_sturdy_lockjaw.sql} | 38 +---- drizzle/migration/meta/0000_snapshot.json | 144 ------------------ drizzle/migration/meta/_journal.json | 4 +- drizzle/migration/schema.ts | 43 +----- package-lock.json | 8 + package.json | 5 +- sample.env | 19 ++- sql/populate_tables.sql | 2 +- 16 files changed, 219 insertions(+), 226 deletions(-) create mode 100644 drizzle/drizzle-pgis/geography.ts create mode 100644 drizzle/drizzle-pgis/geometry.ts create mode 100644 drizzle/drizzle-pgis/index.ts create mode 100644 drizzle/drizzle-pgis/readme.md create mode 100644 drizzle/drizzle-pgis/spatial-type.ts create mode 100644 drizzle/drizzle-pgis/types.ts create mode 100644 drizzle/flow.config.ts rename drizzle/migration/{0000_black_starjammers.sql => 0000_sturdy_lockjaw.sql} (93%) diff --git a/drizzle/api.config.ts b/drizzle/api.config.ts index c49ded7..d812c37 100644 --- a/drizzle/api.config.ts +++ b/drizzle/api.config.ts @@ -5,6 +5,7 @@ export default { schema: "./drizzle/schema/*", dialect: "postgresql", out: "./drizzle/migration", + extensionsFilters: ["postgis"], dbCredentials: { host: process.env.API_DATABASE_HOST!, port: parseInt(process.env.API_DATABASE_PORT!), diff --git a/drizzle/drizzle-pgis/geography.ts b/drizzle/drizzle-pgis/geography.ts new file mode 100644 index 0000000..4a0479f --- /dev/null +++ b/drizzle/drizzle-pgis/geography.ts @@ -0,0 +1,41 @@ +import { SimpleFeature } from "./types"; +import { customType } from "."; +import { + Geometry, + GeometryCollection, + LineString, + MultiLineString, + MultiPoint, + MultiPolygon, + Point, + Polygon, +} from "geojson"; + +export const geography = + (sf: SimpleFeature) => + (name: string, srid = 4326) => + customType<{ data: T }>({ + dataType() { + return `geography(${sf}, ${srid})`; + }, + })(name); + +export const pointGeog = geography(SimpleFeature.POINT); + +export const multiPointGeog = geography(SimpleFeature.MULTI_POINT); + +export const lineStringGeog = geography(SimpleFeature.LINE_STRING); + +export const multiLineStringGeog = geography( + SimpleFeature.MULTI_LINE_STRING, +); + +export const polygonGeog = geography(SimpleFeature.POLYGON); + +export const multiPolygonGeog = geography( + SimpleFeature.MULTI_POLYGON, +); + +export const geometryCollectionGeog = geography( + SimpleFeature.GEOMETRY_COLLECTION, +); diff --git a/drizzle/drizzle-pgis/geometry.ts b/drizzle/drizzle-pgis/geometry.ts new file mode 100644 index 0000000..e218fa8 --- /dev/null +++ b/drizzle/drizzle-pgis/geometry.ts @@ -0,0 +1,41 @@ +import { customType } from "."; +import { + Geometry, + Point, + LineString, + MultiLineString, + GeometryCollection, + MultiPolygon, + Polygon, + MultiPoint, +} from "geojson"; +import { SimpleFeature } from "./types"; + +export const geometry = + (sf: SimpleFeature) => + (name: string, srid = 3857) => + customType<{ data: T }>({ + dataType() { + return `geometry(${sf},${srid})`; + }, + })(name); + +export const pointGeom = geometry(SimpleFeature.POINT); + +export const multiPointGeom = geometry(SimpleFeature.MULTI_POINT); + +export const lineStringGeom = geometry(SimpleFeature.LINE_STRING); + +export const multiLineStringGeom = geometry( + SimpleFeature.MULTI_LINE_STRING, +); + +export const polygonGeom = geometry(SimpleFeature.POLYGON); + +export const multiPolygonGeom = geometry( + SimpleFeature.MULTI_POLYGON, +); + +export const geometryCollectionGeom = geometry( + SimpleFeature.GEOMETRY_COLLECTION, +); diff --git a/drizzle/drizzle-pgis/index.ts b/drizzle/drizzle-pgis/index.ts new file mode 100644 index 0000000..9449a7e --- /dev/null +++ b/drizzle/drizzle-pgis/index.ts @@ -0,0 +1,25 @@ +export { customType } from "drizzle-orm/pg-core"; + +export { + geometry, + pointGeom, + multiPointGeom, + lineStringGeom, + multiLineStringGeom, + polygonGeom, + multiPolygonGeom, + geometryCollectionGeom, +} from "./geometry"; + +export { + geography, + pointGeog, + multiPointGeog, + lineStringGeog, + multiLineStringGeog, + polygonGeog, + multiPolygonGeog, + geometryCollectionGeog, +} from "./geography"; + +export { ST_AsGeoJSON } from "./spatial-type"; diff --git a/drizzle/drizzle-pgis/readme.md b/drizzle/drizzle-pgis/readme.md new file mode 100644 index 0000000..4f571c6 --- /dev/null +++ b/drizzle/drizzle-pgis/readme.md @@ -0,0 +1,24 @@ +## Overview +Custom types for postgis + +## Context for terms +Geometry has slightly different meanings between the broader mapping community and the specific PostGIS typing system. The geojson specification uses Geometry to refer to any coordinates, regardless of whether those coordinates model the Earth as an ellipsoid or a plane. + +In contrast, PostGIS uses Geometry to refer specifically to coordinates that model the Earth as a plane. It uses Geography to refer specifically to coordinates that model the Earth as an ellipsoid. + +## Guidelines for using the types +Within PostGIS, projected coordinate systems typically should be stored in Geometry columns. Conversely, geodetic coordinate systems should be stored in Geography columns. + +Following this guidance, WGS84 (EPSG:4326) should be stored in a Geography column. +Its projected counterpart, Pseudo-Mercator (EPSG:3857), should be stored in a Geometry Column. This package encourages this convention by setting the default srid for geomtry types to 3857 and geography types to 4326. + +## WGS84 is special +The Geometry type predates the Geography type in PostGIS. It also evolved with GeoJSON. GeoJSON uses WSG84 as the default spatial reference system for sharing coordinates. Consequently, the Geometry type historically supported WSG84 by applying [Plate Carée](https://en.wikipedia.org/wiki/Equirectangular_projection) projection. + +Converting from GeoJSON to Geometry types is also easier, thanks to the `ST_GeomFromGeoJSON` PostGIS function. There exists no equivalent function for converting directly from GeoJSON to Geography. Fortunately, the same effect can be achieved by casting the geometry to a geography: +``` +ST_GeomFromGeoJSON(geojson_feature)::geography +``` + +## WGS84 isn't special +Despite Geometry's historical support for WSG84, it is generally safer to move coordinates in this system to a Geography column. Spatial functions for Geometry types assume a flat surface for its calculations. PostGIS will not check whether the coordinates actually satisfy this assumption. Consequently, applying geometry functions to WGS84 coordinates will return meaningless results and PostGIS will fail to emit errors to flag these issues. diff --git a/drizzle/drizzle-pgis/spatial-type.ts b/drizzle/drizzle-pgis/spatial-type.ts new file mode 100644 index 0000000..d208b65 --- /dev/null +++ b/drizzle/drizzle-pgis/spatial-type.ts @@ -0,0 +1,23 @@ +import { PgColumn } from "drizzle-orm/pg-core"; +import { Geometry } from "geojson"; +import { sql } from "drizzle-orm"; + +/** + * https://postgis.net/docs/ST_AsGeoJSON.html + * @returns a geometry or geography as a GeoJSON "geometry" + */ +export const ST_AsGeoJSON = ( + feature: PgColumn<{ + name: string; + tableName: string; + dataType: "custom"; + columnType: "PgCustomColumn"; + data: Geometry; + driverParam: unknown; + notNull: false; + hasDefault: false; + enumValues: undefined; + baseColumn: never; + }>, + maxDecimalDigits = 9, +) => sql`ST_AsGeoJSON(${feature}, ${maxDecimalDigits})`; diff --git a/drizzle/drizzle-pgis/types.ts b/drizzle/drizzle-pgis/types.ts new file mode 100644 index 0000000..031a7e8 --- /dev/null +++ b/drizzle/drizzle-pgis/types.ts @@ -0,0 +1,9 @@ +export enum SimpleFeature { + POINT = "point", + MULTI_POINT = "multiPoint", + LINE_STRING = "lineString", + MULTI_LINE_STRING = "multiLineString", + POLYGON = "polygon", + MULTI_POLYGON = "multiPolygon", + GEOMETRY_COLLECTION = "geometryCollection", +} diff --git a/drizzle/flow.config.ts b/drizzle/flow.config.ts new file mode 100644 index 0000000..33097b9 --- /dev/null +++ b/drizzle/flow.config.ts @@ -0,0 +1,18 @@ +// Drizzle kit configuration +import type { Config } from "drizzle-kit"; + +export default { + schema: "./drizzle/migration/schema.ts", + dialect: "postgresql", + out: "./drizzle/migration", + dbCredentials: { + host: process.env.FLOW_DATABASE_HOST!, + port: parseInt(process.env.FLOW_DATABASE_PORT!), + user: process.env.FLOW_DATABASE_USER, + password: process.env.FLOW_DATABASE_PASSWORD, + database: process.env.FLOW_DATABASE_NAME!, + ssl: process.env.FLOW_DATABASE_ENV !== "development" && { + rejectUnauthorized: false, + }, + }, +} satisfies Config; diff --git a/drizzle/migration/0000_black_starjammers.sql b/drizzle/migration/0000_sturdy_lockjaw.sql similarity index 93% rename from drizzle/migration/0000_black_starjammers.sql rename to drizzle/migration/0000_sturdy_lockjaw.sql index 5f81d26..fc89a29 100644 --- a/drizzle/migration/0000_black_starjammers.sql +++ b/drizzle/migration/0000_sturdy_lockjaw.sql @@ -1,6 +1,5 @@ -- Current sql file was generated after introspecting the database -- If you want to run this migration please uncomment this code before executing migrations -/* DO $$ BEGIN CREATE TYPE "public"."capital_fund_category" AS ENUM('city-non-exempt', 'city-exempt', 'city-cost', 'non-city-state', 'non-city-federal', 'non-city-other', 'non-city-cost', 'total'); EXCEPTION @@ -25,34 +24,6 @@ EXCEPTION WHEN duplicate_object THEN null; END $$; --> statement-breakpoint -CREATE TABLE IF NOT EXISTS "geography_columns" ( - "f_table_catalog" "name", - "f_table_schema" "name", - "f_table_name" "name", - "f_geography_column" "name", - "coord_dimension" integer, - "srid" integer, - "type" text -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "geometry_columns" ( - "f_table_catalog" varchar(256), - "f_table_schema" "name", - "f_table_name" "name", - "f_geometry_column" "name", - "coord_dimension" integer, - "srid" integer, - "type" varchar(30) -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "spatial_ref_sys" ( - "srid" integer PRIMARY KEY NOT NULL, - "auth_name" varchar(256), - "auth_srid" integer, - "srtext" varchar(2048), - "proj4text" varchar(2048) -); ---> statement-breakpoint CREATE TABLE IF NOT EXISTS "agency_budget" ( "code" text PRIMARY KEY NOT NULL, "type" text, @@ -119,7 +90,8 @@ CREATE TABLE IF NOT EXISTS "tax_lot" ( "lot" text NOT NULL, "address" text, "land_use_id" char(2), - "wgs84" "geography" NOT NULL, + -- Caution: hand updated + "wgs84" geography(MultiPolygon,4326) NOT NULL, "li_ft" geometry(MultiPolygon,2263) NOT NULL ); --> statement-breakpoint @@ -152,7 +124,8 @@ CREATE TABLE IF NOT EXISTS "zoning_district_zoning_district_class" ( CREATE TABLE IF NOT EXISTS "zoning_district" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "label" text NOT NULL, - "wgs84" "geography" NOT NULL, + -- Caution: hand updated + "wgs84" geography(MultiPolygon,4326) NOT NULL, "li_ft" geometry(MultiPolygon,2263) NOT NULL ); --> statement-breakpoint @@ -286,5 +259,4 @@ CREATE INDEX IF NOT EXISTS "community_district_mercator_label_index" ON "communi CREATE INDEX IF NOT EXISTS "capital_project_li_ft_m_pnt_index" ON "capital_project" USING gist ("li_ft_m_pnt" gist_geometry_ops_2d);--> statement-breakpoint CREATE INDEX IF NOT EXISTS "capital_project_li_ft_m_poly_index" ON "capital_project" USING gist ("li_ft_m_poly" gist_geometry_ops_2d);--> statement-breakpoint CREATE INDEX IF NOT EXISTS "capital_project_mercator_fill_m_pnt_index" ON "capital_project" USING gist ("mercator_fill_m_pnt" gist_geometry_ops_2d);--> statement-breakpoint -CREATE INDEX IF NOT EXISTS "capital_project_mercator_fill_m_poly_index" ON "capital_project" USING gist ("mercator_fill_m_poly" gist_geometry_ops_2d); -*/ \ No newline at end of file +CREATE INDEX IF NOT EXISTS "capital_project_mercator_fill_m_poly_index" ON "capital_project" USING gist ("mercator_fill_m_poly" gist_geometry_ops_2d); \ No newline at end of file diff --git a/drizzle/migration/meta/0000_snapshot.json b/drizzle/migration/meta/0000_snapshot.json index 4dd51e8..8099128 100644 --- a/drizzle/migration/meta/0000_snapshot.json +++ b/drizzle/migration/meta/0000_snapshot.json @@ -4,150 +4,6 @@ "version": "7", "dialect": "postgresql", "tables": { - "public.geography_columns": { - "name": "geography_columns", - "schema": "", - "columns": { - "f_table_catalog": { - "name": "f_table_catalog", - "type": "name", - "primaryKey": false, - "notNull": false - }, - "f_table_schema": { - "name": "f_table_schema", - "type": "name", - "primaryKey": false, - "notNull": false - }, - "f_table_name": { - "name": "f_table_name", - "type": "name", - "primaryKey": false, - "notNull": false - }, - "f_geography_column": { - "name": "f_geography_column", - "type": "name", - "primaryKey": false, - "notNull": false - }, - "coord_dimension": { - "name": "coord_dimension", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "srid": { - "name": "srid", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.geometry_columns": { - "name": "geometry_columns", - "schema": "", - "columns": { - "f_table_catalog": { - "name": "f_table_catalog", - "type": "varchar(256)", - "primaryKey": false, - "notNull": false - }, - "f_table_schema": { - "name": "f_table_schema", - "type": "name", - "primaryKey": false, - "notNull": false - }, - "f_table_name": { - "name": "f_table_name", - "type": "name", - "primaryKey": false, - "notNull": false - }, - "f_geometry_column": { - "name": "f_geometry_column", - "type": "name", - "primaryKey": false, - "notNull": false - }, - "coord_dimension": { - "name": "coord_dimension", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "srid": { - "name": "srid", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "type": { - "name": "type", - "type": "varchar(30)", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.spatial_ref_sys": { - "name": "spatial_ref_sys", - "schema": "", - "columns": { - "srid": { - "name": "srid", - "type": "integer", - "primaryKey": true, - "notNull": true - }, - "auth_name": { - "name": "auth_name", - "type": "varchar(256)", - "primaryKey": false, - "notNull": false - }, - "auth_srid": { - "name": "auth_srid", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "srtext": { - "name": "srtext", - "type": "varchar(2048)", - "primaryKey": false, - "notNull": false - }, - "proj4text": { - "name": "proj4text", - "type": "varchar(2048)", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, "public.agency_budget": { "name": "agency_budget", "schema": "", diff --git a/drizzle/migration/meta/_journal.json b/drizzle/migration/meta/_journal.json index 19a9779..1f6d73e 100644 --- a/drizzle/migration/meta/_journal.json +++ b/drizzle/migration/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1720450350973, - "tag": "0000_black_starjammers", + "when": 1720469427582, + "tag": "0000_sturdy_lockjaw", "breakpoints": true } ] diff --git a/drizzle/migration/schema.ts b/drizzle/migration/schema.ts index b2b3690..ebdf2f8 100644 --- a/drizzle/migration/schema.ts +++ b/drizzle/migration/schema.ts @@ -1,4 +1,5 @@ -import { pgTable, pgEnum, integer, text, varchar, foreignKey, uuid, char, date, numeric, geometry, index, primaryKey } from "drizzle-orm/pg-core" +import { pgTable, foreignKey, pgEnum, text, uuid, char, date, numeric, geometry, index, primaryKey } from "drizzle-orm/pg-core" +import { multiPolygonGeog, multiPolygonGeom } from "../drizzle-pgis"; import { sql } from "drizzle-orm" export const capital_fund_category = pgEnum("capital_fund_category", ['city-non-exempt', 'city-exempt', 'city-cost', 'non-city-state', 'non-city-federal', 'non-city-other', 'non-city-cost', 'total']) @@ -7,41 +8,6 @@ export const capital_project_fund_stage = pgEnum("capital_project_fund_stage", [ export const category = pgEnum("category", ['Residential', 'Commercial', 'Manufacturing']) -export const geography_columns = pgTable("geography_columns", { - // TODO: failed to parse database type 'name' - f_table_catalog: unknown("f_table_catalog"), - // TODO: failed to parse database type 'name' - f_table_schema: unknown("f_table_schema"), - // TODO: failed to parse database type 'name' - f_table_name: unknown("f_table_name"), - // TODO: failed to parse database type 'name' - f_geography_column: unknown("f_geography_column"), - coord_dimension: integer("coord_dimension"), - srid: integer("srid"), - type: text("type"), -}); - -export const geometry_columns = pgTable("geometry_columns", { - f_table_catalog: varchar("f_table_catalog", { length: 256 }), - // TODO: failed to parse database type 'name' - f_table_schema: unknown("f_table_schema"), - // TODO: failed to parse database type 'name' - f_table_name: unknown("f_table_name"), - // TODO: failed to parse database type 'name' - f_geometry_column: unknown("f_geometry_column"), - coord_dimension: integer("coord_dimension"), - srid: integer("srid"), - type: varchar("type", { length: 30 }), -}); - -export const spatial_ref_sys = pgTable("spatial_ref_sys", { - srid: integer("srid").primaryKey().notNull(), - auth_name: varchar("auth_name", { length: 256 }), - auth_srid: integer("auth_srid"), - srtext: varchar("srtext", { length: 2048 }), - proj4text: varchar("proj4text", { length: 2048 }), -}); - export const agency_budget = pgTable("agency_budget", { code: text("code").primaryKey().notNull(), type: text("type"), @@ -140,8 +106,7 @@ export const tax_lot = pgTable("tax_lot", { lot: text("lot").notNull(), address: text("address"), land_use_id: char("land_use_id", { length: 2 }).references(() => land_use.id), - // TODO: failed to parse database type 'geography' - wgs84: unknown("wgs84").notNull(), + wgs84: multiPolygonGeog("wgs84", 4326).notNull(), li_ft: geometry("li_ft", { type: "multipolygon", srid: 2263 }).notNull(), }); @@ -182,7 +147,7 @@ export const zoning_district = pgTable("zoning_district", { id: uuid("id").defaultRandom().primaryKey().notNull(), label: text("label").notNull(), // TODO: failed to parse database type 'geography' - wgs84: unknown("wgs84").notNull(), + wgs84: multiPolygonGeog("wgs84", 4326).notNull(), li_ft: geometry("li_ft", { type: "multipolygon", srid: 2263 }).notNull(), }); diff --git a/package-lock.json b/package-lock.json index 3f89174..30d58b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "license": "ISC", "devDependencies": { + "@types/geojson": "^7946.0.14", "@types/node": "^20.14.10", "drizzle-kit": "^0.22.8", "drizzle-orm": "^0.31.2", @@ -840,6 +841,13 @@ "node": ">=12" } }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.14.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", diff --git a/package.json b/package.json index d87cc14..6199224 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,14 @@ }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "drizzle:api:pull": "drizzle-kit introspect --config ./drizzle/api.config.ts" + "drizzle:api:pull": "drizzle-kit introspect --config ./drizzle/api.config.ts", + "drizzle:flow:push": "drizzle-kit push --config ./drizzle/flow.config.ts", + "drizzle:flow:migrate": "drizzle-kit push --config ./drizzle/flow.config.ts" }, "author": "", "license": "ISC", "devDependencies": { + "@types/geojson": "^7946.0.14", "@types/node": "^20.14.10", "drizzle-kit": "^0.22.8", "drizzle-orm": "^0.31.2", diff --git a/sample.env b/sample.env index f69e7a8..345dc37 100644 --- a/sample.env +++ b/sample.env @@ -1,9 +1,16 @@ -DRIZZLE_DATABASE_USER=postgres -DRIZZLE_DATABASE_PASSWORD=postgres -DRIZZLE_DATABASE_NAME=zoning -DRIZZLE_DATABASE_PORT=8010 -DRIZZLE_DATABASE_HOST=localhost -DRIZZLE_DATABASE_ENV=development +API_DATABASE_USER=postgres +API_DATABASE_PASSWORD=postgres +API_DATABASE_NAME=zoning +API_DATABASE_PORT=8010 +API_DATABASE_HOST=localhost +API_DATABASE_ENV=development + +FLOW_DATABASE_USER=postgres +FLOW_DATABASE_PASSWORD=postgres +FLOW_DATABASE_NAME=data-flow +FLOW_DATABASE_PORT=8001 +FLOW_DATABASE_HOST=localhost +FLOW_DATABASE_ENV=development BUILD_ENGINE_HOST=ae-data-flow-db-1 BUILD_ENGINE_PORT=5432 diff --git a/sql/populate_tables.sql b/sql/populate_tables.sql index 6a6560a..1fe039d 100644 --- a/sql/populate_tables.sql +++ b/sql/populate_tables.sql @@ -76,7 +76,7 @@ SELECT -- source value to drop the oxford comma CASE WHEN type_category = 'Fixed Asset' OR - type_category = 'Fixed Asset' OR + type_category = 'Lump Sum' OR type_category IS NULL THEN type_category::capital_project_category WHEN type_category = 'ITT, Vehicles, and Equipment'