diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,71 @@
+# Changelog for squealgen
+
+## 0.2.0.0 (2026-02-16)
+
+### Breaking Changes
+
+**Overloaded function naming now uses disambiguated labels.**
+
+Previously, when a PostgreSQL function was overloaded, squealgen would emit only one overload (undefined which one). Now, all representable overloads are emitted with disambiguated labels.
+
+**Function label format:** `name__argtype1__argtype2`
+
+Example for `my_func(int4)` and `my_func(int8)`:
+```haskell
+type Functions =
+  '[ "my_func__int4" ::: Function '[Null PGint4] :=> 'Returns ('Null PGint4)
+   , "my_func__int8" ::: Function '[Null PGint8] :=> 'Returns ('Null PGint8)
+   ]
+```
+
+**Compatibility aliases** are emitted when exactly one overload is representable (others have pseudotype arguments). For example, if `legacy_func(int8)` is representable but `legacy_func(anyelement)` is not:
+```haskell
+type Functions =
+  '[ "legacy_func" ::: Function '[Null PGint8] :=> ...      -- compatibility alias
+   , "legacy_func__int8" ::: Function '[Null PGint8] :=> ... -- disambiguated label
+   ]
+```
+
+### Migration Guide
+
+1. **Find affected functions**: Regenerate your schema and compare to previous version:
+   ```bash
+   squealgen mydb Schema public > Schema.hs.new
+   diff Schema.hs Schema.hs.new
+   ```
+
+2. **Update call sites**: Replace simple function names with disambiguated labels:
+   - Before: `#"my_func"` → After: `#"my_func__int8"` (or appropriate type suffix)
+   - Use grep to find usages: `grep -r '#"my_func"' src/`
+
+3. **No action needed if**:
+   - The function is not overloaded in PostgreSQL
+   - Only one overload is representable (compatibility alias preserved)
+
+### Other Changes
+
+- Generator hardening:
+  - `chosen_schema` is treated as a comma-separated `search_path` fragment and applied safely (quoted identifiers, no raw psql substitution).
+  - The generated `type DB` targets the first schema in the fragment.
+  - Views list output is deterministic (explicit ordering inside `string_agg`).
+- Extensions:
+  - Extension-owned types are emitted as `UnsafePGType` aliases only when referenced.
+  - Generated output includes a comment block listing detected required extensions when extension-owned types are present.
+  - Added an end-to-end `ltree` fixture (`test/Extensions`) and CI installs `postgresql-contrib`.
+- CI/testing:
+  - Dropped the coverage gate (coverage is not meaningful for this generator-only repo).
+  - `make ci` runs `cabal test` with reduced falsify cases (`--falsify-tests 25`) to keep CI runtime bounded.
+
+## Unreleased changes
+
+- Generator hardening:
+  - `chosen_schema` is treated as a comma-separated `search_path` fragment and applied safely (quoted identifiers, no raw psql substitution).
+  - The generated `type DB` targets the first schema in the fragment.
+  - Views list output is deterministic (explicit ordering inside `string_agg`).
+- Extensions:
+  - Extension-owned types are emitted as `UnsafePGType` aliases only when referenced.
+  - Generated output includes a comment block listing detected required extensions when extension-owned types are present.
+  - Added an end-to-end `ltree` fixture (`test/Extensions`) and CI installs `postgresql-contrib`.
+- CI/testing:
+  - Dropped the coverage gate (coverage is not meaningful for this generator-only repo).
+  - `make ci` runs `cabal test` with reduced falsify cases (`--falsify-tests 25`) to keep CI runtime bounded.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Author name here (c) 2020
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Author name here nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,256 @@
+# squealgen
+
+Generate squeal types from a running database.
+
+![CI](https://github.com/mwotton/squealgen/actions/workflows/ci.yml/badge.svg)
+
+## Breaking Changes (v0.2.0.0)
+
+### Overloaded Function Naming
+
+Starting with v0.2.0.0, overloaded PostgreSQL functions now use **disambiguated labels** to ensure type-safe calling:
+
+```haskell
+-- Old (v1.x): Only one overload could be represented, using simple name
+type Functions = '[ "my_func" ::: Function ... ]
+
+-- New (v2.0): All representable overloads use disambiguated labels
+type Functions = '[ "my_func__int4" ::: Function '[Null PGint4] :=> ...
+                  , "my_func__int8" ::: Function '[Null PGint8] :=> ... ]
+```
+
+**Migration Guide:**
+
+1. **Search for simple function names** in your codebase that may have been overloaded:
+   ```bash
+   # Find usages of function labels in your code
+   grep -r '#"my_func"' src/
+   ```
+
+2. **Replace with disambiguated labels**:
+   - Before: `#"my_func"` → After: `#"my_func__int4"` or `#"my_func__int8"` (as appropriate)
+
+3. **Compatibility aliases**: If only ONE overload of a function is representable (others have pseudotype arguments), a compatibility alias is emitted:
+   ```haskell
+   -- Both labels work when only one overload is representable:
+   type Functions = '[ "legacy_func" ::: Function ...      -- compatibility alias
+                     , "legacy_func__int8" ::: Function ... -- disambiguated label
+                     ]
+   ```
+   In this case, existing code using the simple name will continue to work.
+
+4. **Pseudotype functions** (using `anyelement`, `anyarray`, etc.) are not representable and are omitted with a comment—this behavior is unchanged.
+
+## why?
+
+[Squeal](https://hackage.haskell.org/package/squeal-postgresql) is a lovely way to interact with a database, but setting up the initial schema is a struggle.
+By default, it assumes you will be managing and migrating your database with Squeal, and if you are starting
+from scratch, that works great, but if you're managing it some other way, or even just want to test out Squeal
+on an existing database, it's tedious to have to set up the database types and keep them up to date.
+
+## how?
+
+1. clone the repo and change into the directory
+2. Install the executable (recommended):
+
+   ```bash
+   cabal install exe:squealgen --installdir=$HOME/.local/bin --overwrite-policy=always
+   ```
+
+   If you prefer the generated script (dev convenience), you can also run:
+
+   ```bash
+   make prefix=$HOME/.local install
+   ```
+3. If my database is `cooldb`, my Haskell module is `Schema` (file `Schema.hs`), and I want to generate from the `public` schema,
+   I would run `squealgen cooldb Schema public > ~/myproject/src/Schema.hs`.
+
+   Notes:
+
+   - `DBNAME` is passed to `psql -d`, so it can be a database name *or* a libpq connection string/URL.
+   - `MODULENAME` is the Haskell module name (not a file path).
+   - `IMPORTS` (optional) is inserted into the generated module; a convenient pattern is `"... $(cat extra_imports.txt)"`.
+   - `PSQLCMD` can be set to use a non-default `psql` binary.
+
+   `SCHEMA` is treated as a comma-separated `search_path` fragment, so you can pass `public,ext` if you also need `ext` on the path (e.g. for extension-owned types).
+
+You could integrate this in various ways: perhaps just as an initial scaffold, or perhaps integrated as part
+of your build process. A true madman could integrate this into a TH call, but I suspect this would be slow and
+prone to failing (for instance, better never compile any code if you don't have access to the right version
+of psql or a way of spinning up an empty database.)
+
+I highly recommend having a scripted way to bring up a temporary database and run all migrations first. I use
+Jonathan Fischoff's [tmp-postgres](https://hackage.haskell.org/package/tmp-postgres) library and
+recommend it if you're running migrations through Haskell.
+
+## hacking?
+
+My workflow looks like this:
+
+```bash
+make testwatch
+```
+
+`squealgen` is generated from `squealgen.sql` via `./mksquealgen.sh`.
+Treat `squealgen.sql` as the source of truth and do not edit `squealgen` directly.
+
+`./check_squealgen_drift.sh` is run by `make test` and CI to enforce that the checked-in `./squealgen` script matches `squealgen.sql`.
+
+Validation contract:
+
+- Local validation (`make test`): enforce `squealgen` drift parity, regenerate fixture modules, then run `cabal test`.
+- CI validation (`make ci`): enforce drift parity, regenerate fixture modules, then run `cabal test` with reduced falsify cases (`--falsify-tests 25`) to keep runtime bounded.
+
+`SCHEMA` is treated as a comma-separated `search_path` fragment.
+The generator targets only the first schema in the fragment for emitted types, but sets the full `search_path` safely (quoted identifiers).
+
+Extension story:
+
+- If the schema references extension-owned types, squealgen emits opaque `UnsafePGType` aliases (e.g. `type PGltree = UnsafePGType "ltree"`) only when needed.
+- When any extension-owned types are present, generated output includes a comment block listing detected required extensions.
+- Users are responsible for installing extensions via migrations/DDL; CI enforces this via the `test/Extensions` ltree fixture.
+
+Function-overload compatibility notes:
+
+- Generated output always includes deterministic disambiguated overloaded labels (`name__argtokens`).
+- When an overloaded base name has exactly one representable signature, a compatibility alias using the legacy simple name (`name`) is also emitted.
+- When two or more representable overloads remain, no legacy alias is emitted; callers must use the disambiguated labels.
+
+## you'll need
+
+- PostgreSQL client/server tools on your `PATH`: `psql`, `initdb`, `pg_ctl`, `createdb` (used by tests and vendored `vendor/pg_tmp`). On Ubuntu, these are often under `/usr/lib/postgresql/<version>/bin` (e.g. `/usr/lib/postgresql/16/bin`); if `pg_config` is available: `export PATH="$(pg_config --bindir):$PATH"`.
+- make
+- cabal-install
+- `inotifywait` (from `inotify-tools`) if you want to use `make testwatch`.
+
+## what next?
+
+- Remove string-hacking, generate in a more principled way.
+- Improve function-label ergonomics while preserving overload safety and readability.
+- Investigate richer type-level trigger/check representations while preserving current metadata fallback behavior.
+
+## Architecture
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│                              squealgen flow                                  │
+├─────────────────────────────────────────────────────────────────────────────┤
+│                                                                             │
+│   ┌──────────────┐    ┌────────────────┐    ┌──────────────────────────┐   │
+│   │ PostgreSQL   │    │ squealgen.sql  │    │ Generated Schema.hs      │   │
+│   │ Database     │───▶│ (psql script)  │───▶│ (Squeal types)           │   │
+│   │              │    │                │    │                          │   │
+│   │ - tables     │    │ - CTE queries  │    │ - type DB                │   │
+│   │ - views      │    │ - type mapping │    │ - type Schema            │   │
+│   │ - enums      │    │ - emit logic   │    │ - type Tables/Views/...  │   │
+│   │ - functions  │    │                │    │ - function definitions   │   │
+│   └──────────────┘    └────────────────┘    └──────────────────────────┘   │
+│                                                                             │
+│   Input: DBNAME, MODULENAME, SCHEMA                                         │
+│   Output: Haskell module with Squeal type definitions                      │
+│                                                                             │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+The generator queries PostgreSQL system catalogs (`pg_catalog`, `information_schema`) to extract schema metadata, then emits Haskell type definitions compatible with Squeal's type-level DSL.
+
+### Triggers Metadata
+
+The generated output includes a `Triggers` type that provides metadata about PostgreSQL triggers defined on tables in the schema:
+
+```haskell
+-- Example generated output:
+-- triggers
+-- Trigger contract: Triggers is generated metadata and is not composed into Schema.
+type Triggers = 
+  '[ "users_insert_trigger" ::: 'TriggerMetadata
+       '["table" ::: "users", "event" ::: "INSERT", "timing" ::: "BEFORE"]
+   ]
+```
+
+**Important**: The `Triggers` type is **metadata-only** and is NOT composed into the `Schema` type. It cannot be used in Squeal queries. Its purpose is to document what triggers exist in the database for developer reference. Squeal does not provide type-level trigger support.
+
+## Type Mappings
+
+| PostgreSQL Type | Squeal Type |
+|-----------------|-------------|
+| `boolean` | `PGbool` |
+| `int2` / `smallint` | `PGint2` |
+| `int4` / `integer` | `PGint4` |
+| `int8` / `bigint` | `PGint8` |
+| `float4` / `real` | `PGfloat4` |
+| `float8` / `double precision` | `PGfloat8` |
+| `numeric` | `PGnumeric` |
+| `text` | `PGtext` |
+| `varchar` | `PGtext` or `(PGvarchar n)` |
+| `char` | `PGchar` or `(PGvarchar n)` |
+| `bytea` | `PGbytea` |
+| `date` | `PGdate` |
+| `time` | `PGtime` |
+| `timestamp` | `PGtimestamp` |
+| `timestamptz` | `PGtimestamptz` |
+| `interval` | `PGinterval` |
+| `uuid` | `PGuuid` |
+| `inet` | `PGinet` |
+| `json` | `PGjson` |
+| `jsonb` | `PGjsonb` |
+| `oid` | `PGoid` |
+| `array[]` | `(PGvararray ...)` |
+| `enum` | `'PGenum '["label1", "label2", ...]` |
+| `composite` | `'PGcomposite '[...]` |
+| `domain` | Alias to base type |
+
+**Extension types** (ltree, hstore, etc.) are emitted as `UnsafePGType "typename"` aliases.
+
+## Troubleshooting
+
+### "squealgen drift detected"
+
+Run `./mksquealgen.sh` to regenerate the `squealgen` script from `squealgen.sql`, then commit both files. The CI enforces that these stay in sync.
+
+### "initdb: command not found"
+
+PostgreSQL binaries may not be on your PATH. On Ubuntu, try:
+
+```bash
+export PATH="/usr/lib/postgresql/$(ls /usr/lib/postgresql | tail -1)/bin:$PATH"
+```
+
+Or use `pg_config`:
+
+```bash
+export PATH="$(pg_config --bindir):$PATH"
+```
+
+### Generated code doesn't compile
+
+1. Ensure you're using compatible versions of `squeal-postgresql` and GHC.
+2. Check for pseudotype arguments/returns in functions - these are omitted with a comment.
+3. Extension types require `UnsafePGType` - ensure extensions are installed in the database.
+
+### Functions are omitted from output
+
+Functions with pseudotype arguments (e.g., `anyelement`) or returns are not representable in Squeal's type system. Check the generated output for comments like:
+
+```haskell
+-- Omitted function signatures:
+--   my_func(anyelement): pseudotype argument is not representable
+```
+
+### "Croaked: chosen_schema is empty"
+
+The schema argument is required. Provide a valid schema name:
+
+```bash
+squealgen mydb MySchema public > Schema.hs
+```
+
+### Multiple schemas / extensions
+
+Use comma-separated search_path for extensions:
+
+```bash
+squealgen mydb MySchema public,extensions > Schema.hs
+```
+
+Types are generated only for the first schema (`public`), but extension-owned types referenced by it will emit `UnsafePGType` aliases.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,55 @@
+{-# LANGUAGE TypeApplications #-}
+module Main (main) where
+
+import qualified Control.Exception as E
+import           Data.Maybe        (fromMaybe)
+import           System.Environment (getArgs, lookupEnv)
+import           System.Exit        (ExitCode (..), exitFailure, exitWith)
+import           System.IO          (hPutStrLn, stderr)
+import           System.Process     (CreateProcess (..), StdStream (..), createProcess,
+                                    proc, waitForProcess)
+
+import           Paths_squealgen    (getDataFileName)
+
+usage :: IO a
+usage = do
+  hPutStrLn stderr "Usage: squealgen DBNAME MODULENAME SCHEMA [IMPORTS]"
+  exitFailure
+
+main :: IO ()
+main = do
+  args <- getArgs
+  case args of
+    [dbName, moduleName, schemaFragment] -> run dbName moduleName schemaFragment ""
+    [dbName, moduleName, schemaFragment, imports] -> run dbName moduleName schemaFragment imports
+    _ -> usage
+
+run :: String -> String -> String -> String -> IO ()
+run dbName moduleName schemaFragment imports = do
+  e <- E.try @E.SomeException $ do
+    sqlPath <- getDataFileName "squealgen.sql"
+    psqlCmd <- fromMaybe "psql" <$> lookupEnv "PSQLCMD"
+
+    -- Keep this in sync with the generated ./squealgen script.
+    let psql = (proc psqlCmd
+          [ "-X"
+          , "-v", "ON_ERROR_STOP=1"
+          , "-d", dbName
+          , "-v", "chosen_schema=" <> schemaFragment
+          , "-v", "modulename=" <> moduleName
+          , "-v", "extra_imports=" <> imports
+          , "-f", sqlPath
+          ])
+          { std_in = Inherit
+          , std_out = Inherit
+          , std_err = Inherit
+          }
+
+    (_, _, _, ph) <- createProcess psql
+    waitForProcess ph
+
+  case e of
+    Left err -> do
+      hPutStrLn stderr ("error: " <> show err)
+      exitFailure
+    Right code -> exitWith code
diff --git a/squealgen b/squealgen
new file mode 100644
--- /dev/null
+++ b/squealgen
@@ -0,0 +1,1094 @@
+#!/bin/bash
+# NB: this file is generated by mksquealgen.sh, don't edit it directly
+
+if [ "$#" -lt 3 ]; then
+    echo "Usage: squealgen DBNAME MODULENAME SCHEMA [IMPORTS]";
+    exit 1;
+fi
+DBNAME=$1
+MODULENAME=$2
+SCHEMA=$3
+IMPORTS=$4 # this can be empty.
+PSQLCMD=${PSQLCMD:-psql}
+version='0.2.0.0'
+$PSQLCMD -X -d "${DBNAME}" -v chosen_schema="${SCHEMA}" -v modulename="${MODULENAME}" -v extra_imports="${IMPORTS}" -v squealgen_version="${version}" <<"EOF"
+\set QUIET
+\set ON_ERROR_STOP true
+
+-- ============================================================================
+-- THREAT MODEL: Identifier Handling
+-- ============================================================================
+--
+-- This script handles two categories of identifiers:
+--
+-- 1. pg_catalog identifiers (TRUSTED):
+--    System catalog queries (pg_type, pg_class, etc.) return PostgreSQL internal
+--    identifiers. These are controlled by the database system and are trusted.
+--    No quoting is needed when using these in subsequent queries.
+--
+-- 2. Schema names (USER-CONTROLLED):
+--    The :chosen_schema parameter is provided by the user. While this typically
+--    comes from trusted configuration, we treat it as potentially untrusted.
+--    All schema name interpolation uses quote_ident() to prevent SQL injection.
+--
+-- 3. Generated Haskell identifiers:
+--    Table/column names from pg_catalog are emitted as-is into Haskell source.
+--    This is safe because: (a) they come from the database, not user input,
+--    (b) the generated output is compiled, not executed, and (c) Haskell's
+--    type system will reject malformed identifiers at compile time.
+--
+-- ASSUMPTIONS:
+-- - The psql connection has read access to pg_catalog and the target schema
+-- - The target schema contains valid PostgreSQL identifiers
+-- - Output is written to a file that will be compiled by GHC, not executed
+--
+-- ============================================================================
+
+-- ============================================================================
+-- SECTION: Utility Functions
+-- Helper functions for error handling, string manipulation, type declarations,
+-- and aggregation used throughout the generator.
+-- ============================================================================
+
+create or replace function pg_temp.croak(message text) returns text as $$
+begin
+  raise 'Croaked: %', message;
+end;
+$$
+LANGUAGE plpgsql;
+
+create or replace function pg_temp.initCaps(message text) returns text as $$
+begin
+ return replace(initcap(replace(message, '_', ' ')), ' ', '');
+end;
+$$
+LANGUAGE plpgsql;
+
+-- chosen_schema is treated as a comma-separated search_path fragment.
+-- We target the first schema in the fragment for generation, but set the full
+-- search_path safely (quote_ident for each element) without raw psql substitution.
+with raw_parts as (
+  select ordinality as ord, btrim(part) as raw
+    from unnest(string_to_array(:'chosen_schema', ',')) with ordinality as t(part, ordinality)
+), parts as (
+  select ord,
+         case
+           when raw = '' then null
+           when raw ~ '^".*"$' then replace(substr(raw, 2, length(raw) - 2), '""', '"')
+           else raw
+         end as ident
+    from raw_parts
+), nonempty as (
+  select ord, ident
+    from parts
+   where ident is not null
+), derived as (
+  select
+    case
+      when exists (select 1 from nonempty) then (select ident from nonempty order by ord limit 1)
+      else pg_temp.croak('chosen_schema is empty (expected a search_path fragment)')
+    end as primary_schema,
+    case
+      when exists (select 1 from nonempty) then 'information_schema,' || string_agg(quote_ident(ident), ',' order by ord)
+      else null
+    end as safe_search_path
+    from nonempty
+)
+select primary_schema, safe_search_path from derived \gset
+select set_config('search_path', :'safe_search_path', false) \gset
+
+\echo -- | This code was generated by squealgen.
+\echo -- | 
+\echo -- | Generation parameters:
+\echo -- |   Schema: :primary_schema
+\echo -- |   Module: :modulename
+\echo -- |   Version: :squealgen_version
+\echo -- | 
+\echo -- | Edit if you know how it got made and are willing to own it now.
+
+-- Convert PostgreSQL type metadata to a Squeal type expression.
+--
+-- Parameters:
+--   data_type   - Type category from information_schema ('ARRAY', 'USER-DEFINED', etc.)
+--   udt_name    - Underlying type name from pg_type (e.g., '_int4' for int4[])
+--   domain_name - Domain type name if applicable, else NULL
+--   nullable    - Whether the type is nullable (affects Null/NotNull wrapper)
+--   fieldlen    - Character maximum length for varchar types, else NULL
+--
+-- Notes:
+--   - Sources data from both information_schema and pg_type for compatibility
+--   - varchar without length constraint maps to PGtext for simplicity
+--   - Array element nullability uses a conservative NotNull default
+create or replace function pg_temp.type_decl_from(data_type text, udt_name text, domain_name text, nullable bool, fieldlen cardinal_number) RETURNS text as $$
+  select
+  (case
+    when (data_type = 'ARRAY' or data_type = 'A') then
+      format('(PGvararray (%s %s))'
+	    , case when nullable then 'Null' else 'NotNull' end
+	    , case when udt_name = '_varchar' and fieldlen is null then 'PGtext'
+		   when udt_name = '_varchar' then format('(PGvarchar %s)', fieldlen)
+		   else 'PG' || (trim(leading '_' from udt_name::text))
+	      end)
+    else
+      (case
+	 when udt_name = 'varchar' and fieldlen is null then 'PGtext'
+	 when udt_name = 'varchar' then format('(PGvarchar %s)', fieldlen)
+	 else ('PG' || (coalesce(domain_name, udt_name) :: text))
+	 end)
+    end);
+$$
+LANGUAGE sql;
+
+
+create or replace function pg_temp.haddock_comment(message text, indent text default '')
+  returns text as $$
+  select case
+           when message is null or btrim(message) = '' then ''
+           else indent || '-- | ' || regexp_replace(message, E'\n', E'\n' || indent || '--   ', 'g') || E'\n'
+         end;
+$$
+language sql
+immutable;
+
+
+-- Create a function that always returns the first non-NULL item
+CREATE OR REPLACE FUNCTION pg_temp.first_agg ( anyelement, anyelement )
+RETURNS anyelement LANGUAGE SQL IMMUTABLE STRICT AS $$
+	SELECT $1;
+$$;
+
+-- And then wrap an aggregate around it
+CREATE AGGREGATE pg_temp.FIRST (
+	sfunc    = pg_temp.first_agg,
+	basetype = anyelement,
+	stype    = anyelement
+);
+
+-- ============================================================================
+-- SECTION: Haskell Module Header
+-- Emit language pragmas, module declaration, and imports for the generated
+-- Squeal schema module.
+-- ============================================================================
+
+-- PRAGMAS of DOOM
+\echo {-# LANGUAGE DataKinds #-}
+\echo {-# LANGUAGE DeriveGeneric #-}
+\echo {-# LANGUAGE OverloadedLabels #-}
+\echo {-# LANGUAGE FlexibleContexts #-}
+\echo {-# LANGUAGE OverloadedStrings  #-}
+\echo {-# LANGUAGE PolyKinds  #-}
+\echo {-# LANGUAGE TypeApplications #-}
+\echo {-# LANGUAGE TypeOperators #-}
+\echo {-# LANGUAGE GADTs #-}
+\echo {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
+\echo
+\echo module :modulename where
+\echo import Squeal.PostgreSQL
+\echo import GHC.TypeLits(Symbol)
+-- specified imports
+select coalesce(string_agg(format('import %s', s.i)  , E'\n'), '') as imports
+from unnest(string_to_array(:'extra_imports', ',')) as s(i) \gset
+\echo :imports
+
+
+-- ============================================================================
+-- SECTION: Extension and Unsafe Type Detection
+-- Identify types owned by PostgreSQL extensions and emit UnsafePGType aliases.
+-- Also handles built-in types that require manual unsafe treatment.
+-- Key outputs: :required_extensions_comment, :unsafe_type_aliases
+--
+-- The sg_used_type_refs view is shared with used_enums in the Enumerations
+-- section to avoid duplicating the 4-way UNION structure.
+-- ============================================================================
+
+-- Shared view: all types referenced by the schema (tables, views, functions, composites).
+-- Includes typcategory to enable filtering by type kind (e.g., enums, special types).
+create temporary view sg_used_type_refs as
+with used_types as (
+  -- From table/view columns in information_schema
+  select distinct
+    (case when t.typcategory = 'A' then elem.oid else t.oid end) as type_oid,
+    (case when t.typcategory = 'A' then elem.typname else t.typname end) as typname,
+    (case when t.typcategory = 'A' then elem.typcategory else t.typcategory end) as typcategory
+  from information_schema.columns c
+  join pg_catalog.pg_namespace tns on tns.nspname = c.udt_schema
+  join pg_catalog.pg_type t on t.typname = c.udt_name and t.typnamespace = tns.oid
+  left join pg_catalog.pg_type elem on t.typcategory = 'A' and elem.oid = t.typelem
+  where c.table_schema = :'primary_schema'
+  union
+  -- From function arguments in the chosen schema
+  select distinct
+    (case when targ.typcategory = 'A' then elem2.oid else targ.oid end) as type_oid,
+    (case when targ.typcategory = 'A' then elem2.typname else targ.typname end) as typname,
+    (case when targ.typcategory = 'A' then elem2.typcategory else targ.typcategory end) as typcategory
+  from pg_catalog.pg_proc p
+  join pg_catalog.pg_namespace ns on ns.oid = p.pronamespace
+  join unnest(p.proargtypes) as arg(oid) on true
+  join pg_catalog.pg_type targ on targ.oid = arg.oid
+  left join pg_catalog.pg_type elem2 on targ.typcategory = 'A' and elem2.oid = targ.typelem
+  where ns.nspname = :'primary_schema'
+  union
+  -- From function return types in the chosen schema
+  select distinct
+    (case when tret.typcategory = 'A' then elem3.oid else tret.oid end) as type_oid,
+    (case when tret.typcategory = 'A' then elem3.typname else tret.typname end) as typname,
+    (case when tret.typcategory = 'A' then elem3.typcategory else tret.typcategory end) as typcategory
+  from pg_catalog.pg_proc p
+  join pg_catalog.pg_namespace ns on ns.oid = p.pronamespace
+  join pg_catalog.pg_type tret on tret.oid = p.prorettype
+  left join pg_catalog.pg_type elem3 on tret.typcategory = 'A' and elem3.oid = tret.typelem
+  where ns.nspname = :'primary_schema'
+  union
+  -- From composite type attributes in the chosen schema
+  select distinct
+    (case when att_t.typcategory = 'A' then elem4.oid else att_t.oid end) as type_oid,
+    (case when att_t.typcategory = 'A' then elem4.typname else att_t.typname end) as typname,
+    (case when att_t.typcategory = 'A' then elem4.typcategory else att_t.typcategory end) as typcategory
+  from pg_catalog.pg_type comp
+  join pg_catalog.pg_namespace comp_ns on comp_ns.oid = comp.typnamespace
+  join pg_catalog.pg_class comp_cls on comp.typrelid = comp_cls.oid
+  join pg_catalog.pg_attribute a on a.attrelid = comp.typrelid and a.attnum > 0 and not a.attisdropped
+  join pg_catalog.pg_type att_t on att_t.oid = a.atttypid
+  left join pg_catalog.pg_type elem4 on att_t.typcategory = 'A' and elem4.oid = att_t.typelem
+  where comp_ns.nspname = :'primary_schema'
+    and comp.typtype = 'c'
+    and comp_cls.relkind = 'c'
+)
+select type_oid, typname, typcategory from used_types;
+
+-- Convenience view: all used types without category (for backward compatibility)
+create temporary view sg_used_base_types as
+select type_oid, typname from sg_used_type_refs;
+
+create temporary view sg_used_extensions as
+select distinct e.extname, t.typname
+from sg_used_base_types t
+join pg_catalog.pg_depend d
+  on d.classid = 'pg_type'::regclass
+ and d.objid = t.type_oid
+ and d.refclassid = 'pg_extension'::regclass
+ and d.deptype = 'e'
+join pg_catalog.pg_extension e
+  on e.oid = d.refobjid;
+
+with extnames as (select distinct extname from sg_used_extensions)
+select case
+         when count(*) = 0 then ''
+         else '-- Required extensions:' || E'\n' ||
+              string_agg(format('--   %s', extname), E'\n' order by (extname :: text) COLLATE "C") || E'\n'
+       end as required_extensions_comment
+from extnames \gset
+\echo :required_extensions_comment
+
+with manual_unsafe_types as (
+  -- Types that require UnsafePGType aliases because Squeal doesn't map them natively.
+  -- This is a curated list of types that are commonly needed but not in Squeal's PG type family.
+  -- Note: Extension-owned types (ltree, etc.) are handled separately via pg_depend.
+  select distinct t.typname
+  from pg_catalog.pg_type t
+  join pg_catalog.pg_namespace ns on ns.oid = t.typnamespace
+  where ns.nspname = 'pg_catalog'
+    and t.typname in ('name', 'regclass', 'regtype', 'regproc', 'regprocedure', 'regoper', 'regoperator', 'regclass', 'regnamespace', 'regconfig', 'regdictionary')
+), unsafe_types as (
+  select distinct t.typname
+  from sg_used_base_types t
+  left join manual_unsafe_types m on m.typname = t.typname
+  left join (select distinct typname from sg_used_extensions) e on e.typname = t.typname
+  where m.typname is not null or e.typname is not null
+)
+select coalesce(
+         string_agg(
+           format('type PG%s = UnsafePGType "%s"', typname, typname),
+           E'\n'
+           order by (typname :: text) COLLATE "C"),
+         '') as unsafe_type_aliases
+from unsafe_types \gset
+\echo :unsafe_type_aliases
+\echo
+
+-- ============================================================================
+-- SECTION: DB and Schema Type Declarations
+-- Emit the top-level DB type and Schema composition type.
+-- ============================================================================
+
+select format('type DB = ''["%s" ::: Schema]', :'primary_schema') as db \gset
+\echo
+\echo :db
+\echo
+--\echo type Schema = Join (Join Tables Enums) Views
+\echo type Schema = Join Tables (Join Views (Join Enums (Join Functions (Join Composites Domains))))
+\echo -- Trigger contract: Triggers is generated metadata and is not composed into Schema.
+
+
+-- ============================================================================
+-- SECTION: Enumerations
+-- Generate PGenum type definitions for enum types actually used by the schema.
+-- The used_enums CTE finds enums referenced in tables, views, functions, and
+-- composites, then we emit only those definitions to avoid unused clutter.
+-- ============================================================================
+
+-- now we emit all the enumerations
+-- Determine only the enums actually used by the chosen schema (including arrays and function args/returns)
+-- Enums are derived from the shared sg_used_type_refs view (see Extension section).
+-- This avoids duplicating the 4-way UNION structure used for type detection.
+with used_enums as (
+  select typname as enumname
+  from sg_used_type_refs
+  where typcategory = 'E'
+),
+enumerations as (
+  select
+       format(E'type PG%s = ''PGenum\n  ''[%s]',
+                t.typname,
+                string_agg(format('"%s"', e.enumlabel), ', ' order by e.enumsortorder)) as line,
+       format(E'"%1$s" ::: ''Typedef PG%1$s', t.typname) as decl
+  from pg_type t
+     join pg_enum e on t.oid = e.enumtypid
+     join pg_catalog.pg_namespace n ON n.oid = t.typnamespace
+  where t.typname in (select enumname from used_enums)
+  group by t.typname
+  order by (t.typname :: text COLLATE "C")
+)
+select coalesce(string_agg(enumerations.line, E'\n'),'') as enums,
+       format(E'type Enums =\n  (''[%s] :: [(Symbol,SchemumType)])',
+          coalesce(string_agg(enumerations.decl, E',\n  '), '')) as decl
+from enumerations \gset
+\echo -- enums
+\echo :enums
+\echo -- decls
+\echo :decl
+
+-- ============================================================================
+-- SECTION: Composites
+-- Generate PGcomposite type definitions for composite types in the schema.
+-- ============================================================================
+
+with composites as (select
+  format(E'type PG%s = ''PGcomposite ''[%s]', t.typname,
+    string_agg(
+      format(
+        E'"%s" ::: ''NotNull %s',
+        a.attname,
+        pg_temp.type_decl_from(
+          case when t2.typcategory = 'A' then 'ARRAY' else 'USER-DEFINED' end,
+          t2.typname,
+          NULL,
+          false,
+          case when a.atttypmod > 4 then a.atttypmod - 4 else NULL end
+        )
+      )
+      ,', ' order by a.attnum ASC)) as types,
+  format(E'"%1$s" ::: ''Typedef PG%1$s', t.typname) as decl
+from pg_attribute a
+join pg_type t on a.attrelid=t.typrelid
+join pg_type t2 on a.atttypid=t2.oid
+join pg_catalog.pg_namespace n ON n.oid = t.typnamespace
+join pg_class c on t.typrelid=c.oid
+where n.nspname=:'primary_schema'
+and t.typtype='c'
+and c.relkind='c'
+-- this is a bit of a guess, to be honest.
+-- and t.typarray != 0
+group by t.typname)
+select coalesce(string_agg(composites.types, E'\n'), '') as comps,
+       format(E'type Composites =\n  (''[%s] :: [(Symbol,SchemumType)])',
+	      coalesce(string_agg(composites.decl, E',\n  '), '')) as decl
+from composites \gset
+
+\echo :comps
+\echo :decl
+
+\echo
+
+-- ============================================================================
+-- SECTION: Tables (Columns and Constraints)
+-- Generate column definitions and constraint definitions for all tables.
+-- Handles: regular columns, system OID columns for catalogs, primary keys,
+-- foreign keys, unique constraints, and check constraints.
+-- ============================================================================
+
+create temporary view columnDefs as (SELECT tables.table_name,
+             format(E'''[%s]',string_agg(mycolumns.colDef, E'\n  ,' order by mycolumns.ordinal_position)
+             ) as haskCols
+FROM tables
+join (
+  -- Select normal columns from information_schema
+  select columns.table_schema,
+         columns.table_name,
+         columns.ordinal_position,
+         format('"%s" ::: %s :=> %s %s',
+           column_name,
+           case when column_default is null then '''NoDef' else '''Def' end,
+           (case is_nullable
+              when 'YES' then '''Null'
+              when 'NO'  then '''NotNull'
+              else pg_temp.croak ('is_nullable broken somehow: ' || is_nullable)
+            end),
+           -- Note: type_decl_from is called with nullable=false here, which means array elements
+          -- are treated as NotNull. PostgreSQL arrays can have NULL elements, but Squeal's
+          -- type system doesn't distinguish these cases, so we use a conservative default.
+           pg_temp.type_decl_from(data_type, udt_name, domain_name, false, character_maximum_length)
+         ) as colDef
+  from columns
+  where columns.table_schema = :'primary_schema'
+  union all
+  -- Include system OID column for catalogs that expose it (e.g. pg_catalog)
+  select :'primary_schema'::text as table_schema,
+         c.relname          as table_name,
+         0::information_schema.cardinal_number as ordinal_position,
+         '"oid" ::: ''NoDef :=> ''NotNull PGoid' as colDef
+  from pg_catalog.pg_class c
+  join pg_catalog.pg_namespace n on n.oid = c.relnamespace
+  join pg_catalog.pg_attribute a on a.attrelid = c.oid
+  where n.nspname = :'primary_schema'
+    and c.relkind = 'r'
+    and a.attname = 'oid'
+    and a.attnum < 0
+    and not a.attisdropped
+  ) mycolumns on mycolumns.table_name = tables.table_name
+
+WHERE table_type = 'BASE TABLE'
+  AND tables.table_schema = :'primary_schema' --  NOT IN ('pg_catalog', 'information_schema')
+group by tables.table_catalog,
+	 tables.table_schema,
+	 tables.table_name,
+	 tables.table_type,
+	 tables.self_referencing_column_name,
+	 tables.reference_generation,
+	 tables.user_defined_type_catalog,
+	 tables.user_defined_type_schema,
+	 tables.user_defined_type_name,
+	 tables.is_insertable_into,
+	 tables.is_typed,
+	 tables.commit_action
+order by tables.table_name COLLATE "C"
+	 );
+
+
+create temporary view tableComments as (
+  select c.relname as table_name,
+         obj_description(c.oid, 'pg_class') as comment
+  from pg_class c
+  join pg_namespace n on n.oid = c.relnamespace
+  where n.nspname = :'primary_schema'
+    and c.relkind = 'r'
+);
+
+
+-- Constraint metadata view for all constraint types we handle.
+--
+-- Columns:
+--   conoid      - Constraint OID (for stable ordering)
+--   conname     - Constraint name
+--   contype     - Constraint type: 'p' (primary key), 'f' (foreign key),
+--                 'u' (unique), 'c' (check)
+--   nsp         - Schema name containing the constraint
+--   table_name  - Table the constraint is defined on
+--   cols        - Local column names (array, ordered by conkey position)
+--   fnsp        - Referenced schema for FKs (null otherwise)
+--   ftab        - Referenced table for FKs (null otherwise)
+--   fcols       - Referenced column names for FKs (array, ordered by confkey position)
+--   condef      - Human-readable constraint definition via pg_get_constraintdef
+--
+-- Note: fcols uses confkey (referenced columns), NOT conkey (local columns).
+-- This was a bug fix: the original code used conkey which gave wrong ordering
+-- when FK columns were in different order than PK columns.
+create temporary view constraintDefs as (
+SELECT
+  con.oid AS conoid,
+  con.conname AS conname,
+  con.contype AS contype,
+  nsp.nspname AS nsp,
+  tab.relname AS table_name,
+  col.cols,
+  fnsp.nspname AS fnsp,
+  ftab.relname AS ftab,
+  fcol.fcols,
+  pg_catalog.pg_get_constraintdef(con.oid, true) AS condef
+FROM pg_catalog.pg_constraint AS con
+join pg_catalog.pg_namespace n on n.oid = con.connamespace
+INNER JOIN pg_catalog.pg_class AS tab
+ON con.conrelid = tab.oid
+INNER JOIN pg_catalog.pg_namespace AS nsp
+ON con.connamespace = nsp.oid
+LEFT JOIN LATERAL (select array_agg (all col.attname ORDER BY array_position(con.conkey, col.attnum) ASC) cols
+		   from pg_catalog.pg_attribute col
+		   where col.attnum > 0
+		   and not col.attisdropped
+		   and con.conkey @> ARRAY[col.attnum]
+		   and con.conrelid = col.attrelid
+		   ) col on true
+LEFT OUTER JOIN pg_catalog.pg_class AS ftab
+ON con.confrelid = ftab.oid
+LEFT OUTER JOIN pg_catalog.pg_namespace AS fnsp
+ON ftab.relnamespace = fnsp.oid
+--LEFT OUTER JOIN pg_catalog.pg_attribute AS fcol
+--ON con.confkey @> ARRAY[fcol.attnum] AND con.confrelid = fcol.attrelid
+LEFT JOIN LATERAL (select array_agg (all fcol.attname ORDER BY array_position(con.confkey, fcol.attnum) ASC) fcols
+		   from pg_catalog.pg_attribute fcol
+		   where fcol.attnum > 0
+		   and not fcol.attisdropped
+		   and con.confkey @> ARRAY[fcol.attnum]
+		   and con.confrelid = fcol.attrelid
+		   ) fcol on true
+WHERE con.contype IN ('f', 'c', 'p', 'u')
+AND  n.nspname=:'primary_schema'
+GROUP BY
+  con.oid,
+  con.conname,
+  con.contype,
+  nsp.nspname,
+  tab.relname,
+  fnsp.nspname,
+  ftab.relname,
+  col.cols,
+  fcol.fcols,
+  pg_catalog.pg_get_constraintdef(con.oid, true)
+);
+
+select coalesce(string_agg(allDefs.tabData, E'\n'),'') as defs,
+       format(E'type Tables = (''[\n   %s]  :: [(Symbol,SchemumType)])',
+	 coalesce(string_agg(format('"%s" ::: ''Table %sTable', allDefs.table_name, allDefs.cappedName), E'\n  ,' order by allDefs.table_name COLLATE "C" ),'')) as schem
+
+from (
+  select format(E'type %1$sColumns = %2$s\ntype %1$sConstraints = ''[%3$s]\n%4$stype %1$sTable = %1$sConstraints :=> %1$sColumns\n',
+	       replace(initcap(replace(defs.table_name, '_', ' ')), ' ', ''),
+	       max(defs.cols),
+	       coalesce(max(cd.str), ''),
+	       coalesce(pg_temp.haddock_comment((select comment from tableComments where table_name = defs.table_name limit 1)), '')) as tabData,
+	 replace(initcap(replace(defs.table_name, '_', ' ')), ' ', '') as cappedName,
+	 defs.table_name
+from (select table_name, string_agg(columnDefs.haskCols, E'\n  ,') as cols
+      from columnDefs
+      group by table_name
+      order by table_name COLLATE "C") defs
+left join (select table_name,
+	     string_agg(
+               case
+               when contype = 'c' then
+                 format(E'-- | %s\n  "%s" ::: %s',
+                   constraintDefs.condef,
+                   constraintDefs.conname,
+                   case
+                     when constraintDefs.cols is null or cardinality(constraintDefs.cols) = 0
+                       then '''Check ''[]'
+                     else format('''Check ''["%s"]', array_to_string(constraintDefs.cols,'","'))
+                   end)
+               else
+                 format('"%s" ::: %s',constraintDefs.conname,
+	           case contype
+	           when 'p' then format('''PrimaryKey ''["%s"]', array_to_string(cols, '","'))
+	           when 'f' then format('''ForeignKey ''["%s"] "%s" "%s" ''["%s"]', array_to_string(cols,'","'), fnsp, ftab, array_to_string(fcols, '","'))
+	           when 'u' then format('''Unique ''["%s"]', array_to_string(cols,'","'))
+	           else pg_temp.croak (format('bad type %s',contype))
+	           end)
+               end
+			, E'\n  ,' order by (constraintDefs.conname ::text) COLLATE "C") as str
+from constraintDefs
+where contype in ('p', 'f', 'u', 'c')
+group by table_name
+order by (table_name :: text) COLLATE "C" ) cd on cd.table_name = defs.table_name
+group by defs.table_name
+order by defs.table_name COLLATE "C") allDefs \gset
+
+\echo -- schema
+\echo :schem
+\echo
+\echo -- defs
+\echo :defs
+
+\echo -- VIEWS
+
+-- ============================================================================
+-- SECTION: Views
+-- Generate view type definitions. Views are emitted row-by-row to avoid
+-- oversized psql variables.
+-- ============================================================================
+
+create temporary view my_views as (
+SELECT
+  string_agg(
+    format(E'"%s" ::: ''%s %s',
+      cols.column_name,
+      case cols.is_nullable
+        when 'YES' then 'Null'
+        when 'NO'  then 'NotNull'
+        else 'Null'
+      end,
+      pg_temp.type_decl_from(cols.data_type, cols.udt_name, cols.domain_name, false, cols.character_maximum_length)
+    )
+    ,E'\n   ,') as views,
+  cols.table_name as viewname,
+  obj_description(c.oid, 'pg_class') as comment
+FROM information_schema.columns cols
+JOIN pg_catalog.pg_class c ON c.relname = cols.table_name
+JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
+WHERE cols.table_schema = :'primary_schema'
+  AND n.nspname = :'primary_schema'
+  AND c.relkind IN ('v','m')
+GROUP BY cols.table_name, obj_description(c.oid, 'pg_class')
+ORDER BY (cols.table_name :: text) COLLATE "C");
+
+-- select coalesce(string_agg(allDefs.tabData, E'\n'),'') as defs,
+-- Emit the Views type list via a variable (short string),
+-- but print each View type definition row-by-row to avoid oversized variables.
+select format( E'type Views = \n  ''[%s]\n', coalesce(string_agg(format('"%s" ::: ''View %sView', viewname, pg_temp.initCaps(viewname)), ',' order by (viewname :: text) COLLATE "C"), '')) as viewtype
+  from my_views \gset
+\echo :viewtype
+\pset tuples_only on
+\pset format unaligned
+select format( E'%3$stype %1$sView = \n  ''[%2$s]\n'
+             , pg_temp.initCaps(viewname)
+             , views
+             , coalesce(pg_temp.haddock_comment(comment), '') )
+  from my_views
+ order by viewname COLLATE "C";
+\pset tuples_only off
+
+-- ============================================================================
+-- SECTION: Functions
+-- Generate function and procedure type definitions with overload handling.
+--
+-- Function Generation Contract:
+-- - Regular functions: "name" ::: Function '[args] :=> Returns (Null type)
+-- - Set-returning functions: "name" ::: Function '[args] :=> ReturnsTable '[cols]
+-- - Procedures: "name" ::: Procedure '[args]
+--
+-- Overload handling:
+-- - Overloaded functions get disambiguated labels: name__arg1type__arg2type
+-- - If only one overload is representable, a compatibility alias is emitted
+-- - Functions with pseudotype args/returns are omitted with a comment
+-- ============================================================================
+
+\echo -- functions
+
+create temporary view my_functions as
+with function_meta as (
+  select p.oid,
+         p.proname,
+         p.proisstrict,
+         p.prokind,
+         p.proretset,
+         p.proargtypes,
+         p.proargmodes,
+         p.proallargtypes,
+         p.proargnames,
+         ret.typname as ret_type,
+         ret.typcategory as ret_category,
+         ret.typtype as ret_typtype,
+         ret.typrelid as ret_typrelid
+    from pg_catalog.pg_proc p
+    join pg_catalog.pg_namespace ns
+      on ns.oid = p.pronamespace
+    join pg_catalog.pg_type ret
+      on ret.oid = p.prorettype
+   where ns.nspname = :'primary_schema'
+), function_args as (
+  select fm.oid,
+         string_agg(
+           format('%s %s',
+                  case when fm.proisstrict then 'NotNull' else 'Null' end,
+                  pg_temp.type_decl_from(type_arg.typcategory, type_arg.typname, null, false, null)),
+           ',  ' order by args.arg_index
+         ) filter (where args.arg_oid is not null) as arg_decls,
+         string_agg(
+           regexp_replace(
+             lower(
+               case
+                 when type_arg_ns.nspname = 'pg_catalog' then type_arg.typname
+                 else type_arg_ns.nspname || '_' || type_arg.typname
+               end
+             ),
+             '[^a-z0-9]+', '_', 'g'),
+           '__' order by args.arg_index
+         ) filter (where args.arg_oid is not null) as arg_tokens,
+         bool_and(type_arg.typtype <> 'p') filter (where args.arg_oid is not null) as args_representable
+    from function_meta fm
+    left join lateral unnest(fm.proargtypes) with ordinality as args(arg_oid, arg_index)
+      on true
+    left join pg_catalog.pg_type type_arg
+      on type_arg.oid = args.arg_oid
+    left join pg_catalog.pg_namespace type_arg_ns
+      on type_arg_ns.oid = type_arg.typnamespace
+   group by fm.oid
+), function_srf_outcols as (
+  select fm.oid,
+         string_agg(
+           format('"%s" ::: ''Null %s',
+             coalesce((fm.proargnames)[idx.pos], format('column_%s', idx.pos)),
+             pg_temp.type_decl_from(arg_type.typcategory, arg_type.typname, null, false, null)),
+           ',' order by idx.pos
+         ) as row_decls,
+         bool_and(arg_type.typtype <> 'p') as row_representable
+    from function_meta fm
+    join lateral generate_subscripts(coalesce(fm.proallargtypes, array[]::oid[]), 1) as idx(pos)
+      on true
+    join pg_catalog.pg_type arg_type
+      on arg_type.oid = (fm.proallargtypes)[idx.pos]
+   where coalesce((fm.proargmodes)[idx.pos]::text, 'i') in ('o', 't', 'b')
+   group by fm.oid
+), function_srf_composite_cols as (
+  select fm.oid,
+         string_agg(
+           format('"%s" ::: ''Null %s',
+             a.attname,
+             pg_temp.type_decl_from(att_t.typcategory, att_t.typname, null, false, null)),
+           ',' order by a.attnum
+         ) as row_decls,
+         bool_and(att_t.typtype <> 'p') as row_representable
+    from function_meta fm
+    join pg_catalog.pg_attribute a
+      on a.attrelid = fm.ret_typrelid
+     and a.attnum > 0
+     and not a.attisdropped
+    join pg_catalog.pg_type att_t
+      on att_t.oid = a.atttypid
+   where fm.proretset
+     and fm.ret_typtype = 'c'
+   group by fm.oid
+), function_classified as (
+  select fm.oid,
+         fm.proname,
+         fm.prokind,
+         fm.proretset,
+         fm.ret_type,
+         fm.ret_category,
+         count(*) over (partition by fm.proname) as overload_count,
+         coalesce(fa.arg_decls, '') as arg_decls,
+         coalesce(fa.arg_tokens, '') as arg_tokens,
+         (fm.proretset and coalesce(fo.row_decls, fc.row_decls) is not null)
+           or (fm.proretset and fm.ret_typtype <> 'p' and fm.ret_typtype <> 'c') as is_srf,
+         case
+           when fm.proretset and fo.row_decls is not null then fo.row_decls
+           when fm.proretset and fm.ret_typtype = 'c' then fc.row_decls
+           when fm.proretset then format('"result" ::: ''Null %s',
+                               pg_temp.type_decl_from(fm.ret_category, fm.ret_type, null, false, null))
+           else null
+         end as srf_row_decls,
+         case
+           when fm.proretset and fo.row_decls is not null then coalesce(fo.row_representable, true)
+           when fm.proretset and fm.ret_typtype = 'c' then coalesce(fc.row_representable, false)
+           when fm.proretset then fm.ret_typtype <> 'p'
+           else true
+         end as srf_representable,
+         case
+           when fm.proretset and (
+             case
+               when fo.row_decls is not null then not coalesce(fo.row_representable, true)
+               when fm.ret_typtype = 'c' then not coalesce(fc.row_representable, false)
+               else fm.ret_typtype = 'p'
+             end
+           ) then 'set-returning pseudotype return is not representable'
+           when not coalesce(fa.args_representable, true) then 'pseudotype argument is not representable'
+           when not fm.proretset and fm.prokind <> 'p' and fm.ret_typtype = 'p' then 'pseudotype return is not representable'
+           else null
+         end as omission_reason
+    from function_meta fm
+    left join function_args fa
+      on fa.oid = fm.oid
+    left join function_srf_outcols fo
+      on fo.oid = fm.oid
+    left join function_srf_composite_cols fc
+      on fc.oid = fm.oid
+), function_labeled as (
+  select funcs.*,
+         count(*) filter (where funcs.omission_reason is null) over (partition by funcs.proname) as representable_overload_count,
+         case
+           when funcs.overload_count > 1
+             then funcs.proname || '__' || coalesce(nullif(funcs.arg_tokens, ''), 'noargs')
+           else funcs.proname
+         end as disambiguated_label
+    from function_classified funcs
+)
+select proname,
+       prokind,
+       proretset,
+       arg_decls,
+       arg_tokens,
+       ret_type,
+       ret_category,
+       is_srf,
+       srf_row_decls,
+       omission_reason,
+       disambiguated_label as label,
+       case
+         when overload_count > 1
+              and omission_reason is null
+              and representable_overload_count = 1
+           then proname
+         else null
+       end as compatibility_alias
+  from function_labeled;
+
+select format(E'type Functions = \n  ''[ %s ]'
+     , coalesce(string_agg(
+         case
+           when entries.prokind = 'p'
+             then format(E'"%s" ::: ''Procedure ''[ %s ]',
+                         entries.label,
+                         entries.arg_decls)
+           when entries.is_srf
+             then format(E'"%s" ::: Function (''[ %s ] :=> ''ReturnsTable ''[%s])',
+                         entries.label,
+                         entries.arg_decls,
+                         entries.srf_row_decls)
+           else format(E'"%s" ::: Function (''[ %s ] :=> ''Returns ( ''Null %s) )',
+                       entries.label,
+                       entries.arg_decls,
+                       pg_temp.type_decl_from(entries.ret_category, entries.ret_type, null, false, null))
+         end,
+         E'\n   , ' order by (entries.label :: text) COLLATE "C"), '')
+       ) as functions
+from (
+  select funcs.label,
+         funcs.prokind,
+         funcs.is_srf,
+         funcs.arg_decls,
+         funcs.srf_row_decls,
+         funcs.ret_category,
+         funcs.ret_type
+    from my_functions funcs
+   where funcs.omission_reason is null
+  union all
+  select funcs.compatibility_alias as label,
+         funcs.prokind,
+         funcs.is_srf,
+         funcs.arg_decls,
+         funcs.srf_row_decls,
+         funcs.ret_category,
+         funcs.ret_type
+    from my_functions funcs
+   where funcs.omission_reason is null
+     and funcs.compatibility_alias is not null
+) entries \gset
+\echo :functions
+
+-- Emit warning note if any function has multiple representable overloads.
+-- Callers must use disambiguated labels (name__argtype) in these cases.
+-- Note: We detect overloads by counting how many distinct labels share the same proname.
+with overload_stats as (
+  select proname,
+         count(*) as representable_count
+    from my_functions
+   where omission_reason is null
+   group by proname
+   having count(*) > 1
+)
+select case
+         when count(*) = 0 then ''
+         else E'\n-- Overloaded functions with multiple representable signatures:\n'
+              || string_agg(
+                   format(E'--   %s has %s representable overloads - use disambiguated labels',
+                     os.proname,
+                     os.representable_count),
+                   E'\n' order by (os.proname :: text) COLLATE "C")
+       end as overloaded_warning
+  from overload_stats os \gset
+\echo :overloaded_warning
+
+select case
+         when count(*) = 0 then '-- Omitted function signatures: none'
+         else E'-- Omitted function signatures:\n'
+              || string_agg(
+                   format(E'--   %s(%s): %s',
+                     funcs.proname,
+                     coalesce(nullif(replace(funcs.arg_tokens, '__', ', '), ''), 'noargs'),
+                     funcs.omission_reason),
+                   E'\n' order by (funcs.proname :: text) COLLATE "C",
+                                 (funcs.arg_tokens :: text) COLLATE "C")
+       end as omitted_function_signatures
+  from my_functions funcs
+ where funcs.omission_reason is not null
+   and not funcs.proretset \gset
+\echo :omitted_function_signatures
+
+select case
+         when count(*) = 0 then '-- Omitted SRF signatures: none'
+         else E'-- Omitted SRF signatures:\n'
+              || string_agg(
+                   format(E'--   %s(%s): %s',
+                     funcs.proname,
+                     coalesce(nullif(replace(funcs.arg_tokens, '__', ', '), ''), 'noargs'),
+                     funcs.omission_reason),
+                   E'\n' order by (funcs.proname :: text) COLLATE "C",
+                                 (funcs.arg_tokens :: text) COLLATE "C")
+       end as omitted_srf_signatures
+  from my_functions funcs
+ where funcs.omission_reason is not null
+   and funcs.proretset \gset
+\echo :omitted_srf_signatures
+
+-- ============================================================================
+-- SECTION: Domains
+-- Generate domain type definitions. Domain check constraints are emitted
+-- as Haddock notes only (not representable in Squeal types).
+-- ============================================================================
+
+SELECT format('type Domains = ''[%s]',
+	 coalesce(string_agg(format(E'"%s" ::: ''Typedef PG%s',
+					   pg_type.typname, p2.typname  ),
+			E'\n   ,' ), '')) as domains,
+       coalesce(string_agg(format ('type PG%s = PG%s', pg_type.typname, p2.typname ) , E'\n' order by (pg_type.typname :: text) COLLATE "C" asc, (p2.typname :: text) COLLATE "C" asc), '') as decls
+FROM pg_catalog.pg_type
+JOIN pg_catalog.pg_namespace ON pg_namespace.oid = pg_type.typnamespace
+join pg_catalog.pg_type p2 on pg_type.typbasetype = p2.oid
+WHERE pg_type.typtype = 'd' AND nspname = :'primary_schema' \gset
+
+\echo :domains
+\echo :decls
+
+select case
+         when count(*) = 0 then '-- Check-constraint fallback notes: none'
+         else E'-- Check-constraint fallback notes:\n'
+              || string_agg(line, E'\n' order by (line :: text) COLLATE "C")
+       end as omitted_fallback_check_constraints
+from (
+  select format(
+           E'--   %s.%s %s: expression emitted as Haddock note only (%s)',
+           c.nsp,
+           c.table_name,
+           c.conname,
+           c.condef
+         ) as line
+  from constraintDefs c
+  where c.contype = 'c'
+  union all
+  select format(
+           E'--   domain %s.%s %s: not representable in Domains typedef output (%s)',
+           dn.nspname,
+           dt.typname,
+           con.conname,
+           pg_catalog.pg_get_constraintdef(con.oid, true)
+         ) as line
+  from pg_catalog.pg_constraint con
+  join pg_catalog.pg_type dt
+    on dt.oid = con.contypid
+  join pg_catalog.pg_namespace dn
+    on dn.oid = dt.typnamespace
+  where con.contype = 'c'
+    and dn.nspname = :'primary_schema'
+) fallback_checks \gset
+\echo :omitted_fallback_check_constraints
+
+-- ============================================================================
+-- SECTION: Triggers
+-- Generate trigger metadata. Triggers are NOT composed into Schema; they are
+-- emitted as a separate Triggers type for reference/metadata purposes only.
+-- Trigger definitions are captured via pg_get_triggerdef when available.
+-- ============================================================================
+
+create temporary view triggerDefs as (
+  select t.oid as tgoid,
+         t.tgname as trigger_name,
+         rel.relname as relation_name,
+         case
+           when (t.tgtype & 2) <> 0 then 'BEFORE'
+           when (t.tgtype & 64) <> 0 then 'INSTEAD OF'
+           else 'AFTER'
+         end as trigger_timing,
+         case
+           when (t.tgtype & 1) <> 0 then 'ROW'
+           else 'STATEMENT'
+         end as trigger_level,
+         array_to_string(
+           array_remove(
+             array[
+               case when (t.tgtype & 4) <> 0 then 'INSERT' end,
+               case when (t.tgtype & 8) <> 0 then 'DELETE' end,
+               case when (t.tgtype & 16) <> 0 then 'UPDATE' end,
+               case when (t.tgtype & 32) <> 0 then 'TRUNCATE' end
+             ],
+             null
+           ),
+           ' OR '
+         ) as trigger_events,
+         (t.tgconstraint <> 0) as is_constraint_trigger,
+         pg_catalog.pg_get_triggerdef(t.oid, true) as trigger_definition
+  from pg_catalog.pg_trigger t
+  join pg_catalog.pg_class rel
+    on rel.oid = t.tgrelid
+  join pg_catalog.pg_namespace n
+    on n.oid = rel.relnamespace
+  where n.nspname = :'primary_schema'
+    and not t.tgisinternal
+);
+
+\echo
+\echo -- triggers
+\echo -- Trigger contract: Triggers is generated metadata and is not composed into Schema.
+select case
+         when count(*) = 0 then E'type Triggers = \n  ''[]'
+         else format(
+           E'type Triggers = \n  ''[ %s ]',
+           string_agg(
+             format(
+               E'''("%s", "%s")',
+               replace(td.trigger_name, '"', E'\\\"'),
+               replace(
+                 replace(
+                   coalesce(
+                     td.trigger_definition,
+                     format(
+                       '%s %s %s ON %s%s',
+                       td.trigger_timing,
+                       td.trigger_level,
+                       td.trigger_events,
+                       td.relation_name,
+                       case when td.is_constraint_trigger then ' [constraint]' else '' end
+                     )
+                   ),
+                   E'\\',
+                   E'\\\\'
+                 ),
+                 '"',
+                 E'\\\"'
+               )
+             ),
+             E'\n   , ' order by (td.trigger_name :: text) COLLATE "C",
+                                (td.relation_name :: text) COLLATE "C",
+                                td.tgoid
+           )
+         )
+       end as triggers
+  from triggerDefs td \gset
+\echo :triggers
+
+select coalesce(
+         string_agg(
+           format(
+             E'-- | Trigger %s on %s: full definition unavailable, emitted metadata fallback.',
+             td.trigger_name,
+             td.relation_name
+           ),
+          E'\n' order by (td.trigger_name :: text) COLLATE "C",
+                         (td.relation_name :: text) COLLATE "C",
+                         td.tgoid
+        ),
+        ''
+      ) as fallback_trigger_haddocks
+  from triggerDefs td
+ where td.trigger_definition is null \gset
+\echo :fallback_trigger_haddocks
+
+select case
+         when count(*) = 0 then '-- Trigger fallback notes: none'
+         else E'-- Trigger fallback notes:\n'
+              || string_agg(
+                   format(
+                     E'--   %s on %s: full definition unavailable; emitted metadata fallback (%s %s %s%s)',
+                     td.trigger_name,
+                     td.relation_name,
+                     td.trigger_timing,
+                     td.trigger_level,
+                     td.trigger_events,
+                     case when td.is_constraint_trigger then ', constraint trigger' else '' end
+                   ),
+                   E'\n' order by (td.trigger_name :: text) COLLATE "C",
+                                  (td.relation_name :: text) COLLATE "C",
+                                  td.tgoid
+                 )
+       end as omitted_fallback_triggers
+  from triggerDefs td
+ where td.trigger_definition is null \gset
+\echo :omitted_fallback_triggers
+EOF
diff --git a/squealgen.cabal b/squealgen.cabal
new file mode 100644
--- /dev/null
+++ b/squealgen.cabal
@@ -0,0 +1,82 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.38.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 538f841975036e6035b1dc27b4611c2bc7ccf735c7acea6790dcee38d4135670
+
+name:           squealgen
+version:        0.2.0.0
+synopsis:       generate squeal types from an existing database
+description:    Please see the README on GitHub at <https://github.com/mwotton/squealgen#readme>
+category:       Database
+homepage:       https://github.com/mwotton/squealgen#readme
+bug-reports:    https://github.com/mwotton/squealgen/issues
+author:         Mark Wotton
+maintainer:     mwotton@gmail.com
+copyright:      2020 Mark Wotton
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    ChangeLog.md
+    squealgen
+data-files:
+    squealgen.sql
+
+source-repository head
+  type: git
+  location: https://github.com/mwotton/squealgen
+
+executable squealgen
+  main-is: Main.hs
+  other-modules:
+      Paths_squealgen
+  hs-source-dirs:
+      app
+  build-depends:
+      base >=4.7 && <5
+    , process >=1.6 && <2
+  default-language: Haskell2010
+
+test-suite tests
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      EmptySchema.DBSpec
+      EmptySchema.Public
+  hs-source-dirs:
+      test
+  default-extensions:
+      DataKinds
+      DeriveGeneric
+      OverloadedLabels
+      FlexibleContexts
+      TypeApplications
+      TypeOperators
+      GADTs
+  build-depends:
+      base >=4.7 && <5
+    , bytestring
+    , containers
+    , directory
+    , falsify
+    , filepath
+    , generics-sop
+    , hspec
+    , hspec-expectations-lifted
+    , iproute
+    , mtl
+    , network-ip
+    , postgresql-binary
+    , process
+    , squeal-postgresql >=0.9.2.0
+    , tasty
+    , tasty-hspec
+    , temporary
+    , text
+    , tmp-postgres >=1.34.1.0
+    , unliftio
+  default-language: Haskell2010
diff --git a/squealgen.sql b/squealgen.sql
new file mode 100644
--- /dev/null
+++ b/squealgen.sql
@@ -0,0 +1,1079 @@
+\set QUIET
+\set ON_ERROR_STOP true
+
+-- ============================================================================
+-- THREAT MODEL: Identifier Handling
+-- ============================================================================
+--
+-- This script handles two categories of identifiers:
+--
+-- 1. pg_catalog identifiers (TRUSTED):
+--    System catalog queries (pg_type, pg_class, etc.) return PostgreSQL internal
+--    identifiers. These are controlled by the database system and are trusted.
+--    No quoting is needed when using these in subsequent queries.
+--
+-- 2. Schema names (USER-CONTROLLED):
+--    The :chosen_schema parameter is provided by the user. While this typically
+--    comes from trusted configuration, we treat it as potentially untrusted.
+--    All schema name interpolation uses quote_ident() to prevent SQL injection.
+--
+-- 3. Generated Haskell identifiers:
+--    Table/column names from pg_catalog are emitted as-is into Haskell source.
+--    This is safe because: (a) they come from the database, not user input,
+--    (b) the generated output is compiled, not executed, and (c) Haskell's
+--    type system will reject malformed identifiers at compile time.
+--
+-- ASSUMPTIONS:
+-- - The psql connection has read access to pg_catalog and the target schema
+-- - The target schema contains valid PostgreSQL identifiers
+-- - Output is written to a file that will be compiled by GHC, not executed
+--
+-- ============================================================================
+
+-- ============================================================================
+-- SECTION: Utility Functions
+-- Helper functions for error handling, string manipulation, type declarations,
+-- and aggregation used throughout the generator.
+-- ============================================================================
+
+create or replace function pg_temp.croak(message text) returns text as $$
+begin
+  raise 'Croaked: %', message;
+end;
+$$
+LANGUAGE plpgsql;
+
+create or replace function pg_temp.initCaps(message text) returns text as $$
+begin
+ return replace(initcap(replace(message, '_', ' ')), ' ', '');
+end;
+$$
+LANGUAGE plpgsql;
+
+-- chosen_schema is treated as a comma-separated search_path fragment.
+-- We target the first schema in the fragment for generation, but set the full
+-- search_path safely (quote_ident for each element) without raw psql substitution.
+with raw_parts as (
+  select ordinality as ord, btrim(part) as raw
+    from unnest(string_to_array(:'chosen_schema', ',')) with ordinality as t(part, ordinality)
+), parts as (
+  select ord,
+         case
+           when raw = '' then null
+           when raw ~ '^".*"$' then replace(substr(raw, 2, length(raw) - 2), '""', '"')
+           else raw
+         end as ident
+    from raw_parts
+), nonempty as (
+  select ord, ident
+    from parts
+   where ident is not null
+), derived as (
+  select
+    case
+      when exists (select 1 from nonempty) then (select ident from nonempty order by ord limit 1)
+      else pg_temp.croak('chosen_schema is empty (expected a search_path fragment)')
+    end as primary_schema,
+    case
+      when exists (select 1 from nonempty) then 'information_schema,' || string_agg(quote_ident(ident), ',' order by ord)
+      else null
+    end as safe_search_path
+    from nonempty
+)
+select primary_schema, safe_search_path from derived \gset
+select set_config('search_path', :'safe_search_path', false) \gset
+
+\echo -- | This code was generated by squealgen.
+\echo -- | 
+\echo -- | Generation parameters:
+\echo -- |   Schema: :primary_schema
+\echo -- |   Module: :modulename
+\echo -- |   Version: :squealgen_version
+\echo -- | 
+\echo -- | Edit if you know how it got made and are willing to own it now.
+
+-- Convert PostgreSQL type metadata to a Squeal type expression.
+--
+-- Parameters:
+--   data_type   - Type category from information_schema ('ARRAY', 'USER-DEFINED', etc.)
+--   udt_name    - Underlying type name from pg_type (e.g., '_int4' for int4[])
+--   domain_name - Domain type name if applicable, else NULL
+--   nullable    - Whether the type is nullable (affects Null/NotNull wrapper)
+--   fieldlen    - Character maximum length for varchar types, else NULL
+--
+-- Notes:
+--   - Sources data from both information_schema and pg_type for compatibility
+--   - varchar without length constraint maps to PGtext for simplicity
+--   - Array element nullability uses a conservative NotNull default
+create or replace function pg_temp.type_decl_from(data_type text, udt_name text, domain_name text, nullable bool, fieldlen cardinal_number) RETURNS text as $$
+  select
+  (case
+    when (data_type = 'ARRAY' or data_type = 'A') then
+      format('(PGvararray (%s %s))'
+	    , case when nullable then 'Null' else 'NotNull' end
+	    , case when udt_name = '_varchar' and fieldlen is null then 'PGtext'
+		   when udt_name = '_varchar' then format('(PGvarchar %s)', fieldlen)
+		   else 'PG' || (trim(leading '_' from udt_name::text))
+	      end)
+    else
+      (case
+	 when udt_name = 'varchar' and fieldlen is null then 'PGtext'
+	 when udt_name = 'varchar' then format('(PGvarchar %s)', fieldlen)
+	 else ('PG' || (coalesce(domain_name, udt_name) :: text))
+	 end)
+    end);
+$$
+LANGUAGE sql;
+
+
+create or replace function pg_temp.haddock_comment(message text, indent text default '')
+  returns text as $$
+  select case
+           when message is null or btrim(message) = '' then ''
+           else indent || '-- | ' || regexp_replace(message, E'\n', E'\n' || indent || '--   ', 'g') || E'\n'
+         end;
+$$
+language sql
+immutable;
+
+
+-- Create a function that always returns the first non-NULL item
+CREATE OR REPLACE FUNCTION pg_temp.first_agg ( anyelement, anyelement )
+RETURNS anyelement LANGUAGE SQL IMMUTABLE STRICT AS $$
+	SELECT $1;
+$$;
+
+-- And then wrap an aggregate around it
+CREATE AGGREGATE pg_temp.FIRST (
+	sfunc    = pg_temp.first_agg,
+	basetype = anyelement,
+	stype    = anyelement
+);
+
+-- ============================================================================
+-- SECTION: Haskell Module Header
+-- Emit language pragmas, module declaration, and imports for the generated
+-- Squeal schema module.
+-- ============================================================================
+
+-- PRAGMAS of DOOM
+\echo {-# LANGUAGE DataKinds #-}
+\echo {-# LANGUAGE DeriveGeneric #-}
+\echo {-# LANGUAGE OverloadedLabels #-}
+\echo {-# LANGUAGE FlexibleContexts #-}
+\echo {-# LANGUAGE OverloadedStrings  #-}
+\echo {-# LANGUAGE PolyKinds  #-}
+\echo {-# LANGUAGE TypeApplications #-}
+\echo {-# LANGUAGE TypeOperators #-}
+\echo {-# LANGUAGE GADTs #-}
+\echo {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
+\echo
+\echo module :modulename where
+\echo import Squeal.PostgreSQL
+\echo import GHC.TypeLits(Symbol)
+-- specified imports
+select coalesce(string_agg(format('import %s', s.i)  , E'\n'), '') as imports
+from unnest(string_to_array(:'extra_imports', ',')) as s(i) \gset
+\echo :imports
+
+
+-- ============================================================================
+-- SECTION: Extension and Unsafe Type Detection
+-- Identify types owned by PostgreSQL extensions and emit UnsafePGType aliases.
+-- Also handles built-in types that require manual unsafe treatment.
+-- Key outputs: :required_extensions_comment, :unsafe_type_aliases
+--
+-- The sg_used_type_refs view is shared with used_enums in the Enumerations
+-- section to avoid duplicating the 4-way UNION structure.
+-- ============================================================================
+
+-- Shared view: all types referenced by the schema (tables, views, functions, composites).
+-- Includes typcategory to enable filtering by type kind (e.g., enums, special types).
+create temporary view sg_used_type_refs as
+with used_types as (
+  -- From table/view columns in information_schema
+  select distinct
+    (case when t.typcategory = 'A' then elem.oid else t.oid end) as type_oid,
+    (case when t.typcategory = 'A' then elem.typname else t.typname end) as typname,
+    (case when t.typcategory = 'A' then elem.typcategory else t.typcategory end) as typcategory
+  from information_schema.columns c
+  join pg_catalog.pg_namespace tns on tns.nspname = c.udt_schema
+  join pg_catalog.pg_type t on t.typname = c.udt_name and t.typnamespace = tns.oid
+  left join pg_catalog.pg_type elem on t.typcategory = 'A' and elem.oid = t.typelem
+  where c.table_schema = :'primary_schema'
+  union
+  -- From function arguments in the chosen schema
+  select distinct
+    (case when targ.typcategory = 'A' then elem2.oid else targ.oid end) as type_oid,
+    (case when targ.typcategory = 'A' then elem2.typname else targ.typname end) as typname,
+    (case when targ.typcategory = 'A' then elem2.typcategory else targ.typcategory end) as typcategory
+  from pg_catalog.pg_proc p
+  join pg_catalog.pg_namespace ns on ns.oid = p.pronamespace
+  join unnest(p.proargtypes) as arg(oid) on true
+  join pg_catalog.pg_type targ on targ.oid = arg.oid
+  left join pg_catalog.pg_type elem2 on targ.typcategory = 'A' and elem2.oid = targ.typelem
+  where ns.nspname = :'primary_schema'
+  union
+  -- From function return types in the chosen schema
+  select distinct
+    (case when tret.typcategory = 'A' then elem3.oid else tret.oid end) as type_oid,
+    (case when tret.typcategory = 'A' then elem3.typname else tret.typname end) as typname,
+    (case when tret.typcategory = 'A' then elem3.typcategory else tret.typcategory end) as typcategory
+  from pg_catalog.pg_proc p
+  join pg_catalog.pg_namespace ns on ns.oid = p.pronamespace
+  join pg_catalog.pg_type tret on tret.oid = p.prorettype
+  left join pg_catalog.pg_type elem3 on tret.typcategory = 'A' and elem3.oid = tret.typelem
+  where ns.nspname = :'primary_schema'
+  union
+  -- From composite type attributes in the chosen schema
+  select distinct
+    (case when att_t.typcategory = 'A' then elem4.oid else att_t.oid end) as type_oid,
+    (case when att_t.typcategory = 'A' then elem4.typname else att_t.typname end) as typname,
+    (case when att_t.typcategory = 'A' then elem4.typcategory else att_t.typcategory end) as typcategory
+  from pg_catalog.pg_type comp
+  join pg_catalog.pg_namespace comp_ns on comp_ns.oid = comp.typnamespace
+  join pg_catalog.pg_class comp_cls on comp.typrelid = comp_cls.oid
+  join pg_catalog.pg_attribute a on a.attrelid = comp.typrelid and a.attnum > 0 and not a.attisdropped
+  join pg_catalog.pg_type att_t on att_t.oid = a.atttypid
+  left join pg_catalog.pg_type elem4 on att_t.typcategory = 'A' and elem4.oid = att_t.typelem
+  where comp_ns.nspname = :'primary_schema'
+    and comp.typtype = 'c'
+    and comp_cls.relkind = 'c'
+)
+select type_oid, typname, typcategory from used_types;
+
+-- Convenience view: all used types without category (for backward compatibility)
+create temporary view sg_used_base_types as
+select type_oid, typname from sg_used_type_refs;
+
+create temporary view sg_used_extensions as
+select distinct e.extname, t.typname
+from sg_used_base_types t
+join pg_catalog.pg_depend d
+  on d.classid = 'pg_type'::regclass
+ and d.objid = t.type_oid
+ and d.refclassid = 'pg_extension'::regclass
+ and d.deptype = 'e'
+join pg_catalog.pg_extension e
+  on e.oid = d.refobjid;
+
+with extnames as (select distinct extname from sg_used_extensions)
+select case
+         when count(*) = 0 then ''
+         else '-- Required extensions:' || E'\n' ||
+              string_agg(format('--   %s', extname), E'\n' order by (extname :: text) COLLATE "C") || E'\n'
+       end as required_extensions_comment
+from extnames \gset
+\echo :required_extensions_comment
+
+with manual_unsafe_types as (
+  -- Types that require UnsafePGType aliases because Squeal doesn't map them natively.
+  -- This is a curated list of types that are commonly needed but not in Squeal's PG type family.
+  -- Note: Extension-owned types (ltree, etc.) are handled separately via pg_depend.
+  select distinct t.typname
+  from pg_catalog.pg_type t
+  join pg_catalog.pg_namespace ns on ns.oid = t.typnamespace
+  where ns.nspname = 'pg_catalog'
+    and t.typname in ('name', 'regclass', 'regtype', 'regproc', 'regprocedure', 'regoper', 'regoperator', 'regclass', 'regnamespace', 'regconfig', 'regdictionary')
+), unsafe_types as (
+  select distinct t.typname
+  from sg_used_base_types t
+  left join manual_unsafe_types m on m.typname = t.typname
+  left join (select distinct typname from sg_used_extensions) e on e.typname = t.typname
+  where m.typname is not null or e.typname is not null
+)
+select coalesce(
+         string_agg(
+           format('type PG%s = UnsafePGType "%s"', typname, typname),
+           E'\n'
+           order by (typname :: text) COLLATE "C"),
+         '') as unsafe_type_aliases
+from unsafe_types \gset
+\echo :unsafe_type_aliases
+\echo
+
+-- ============================================================================
+-- SECTION: DB and Schema Type Declarations
+-- Emit the top-level DB type and Schema composition type.
+-- ============================================================================
+
+select format('type DB = ''["%s" ::: Schema]', :'primary_schema') as db \gset
+\echo
+\echo :db
+\echo
+--\echo type Schema = Join (Join Tables Enums) Views
+\echo type Schema = Join Tables (Join Views (Join Enums (Join Functions (Join Composites Domains))))
+\echo -- Trigger contract: Triggers is generated metadata and is not composed into Schema.
+
+
+-- ============================================================================
+-- SECTION: Enumerations
+-- Generate PGenum type definitions for enum types actually used by the schema.
+-- The used_enums CTE finds enums referenced in tables, views, functions, and
+-- composites, then we emit only those definitions to avoid unused clutter.
+-- ============================================================================
+
+-- now we emit all the enumerations
+-- Determine only the enums actually used by the chosen schema (including arrays and function args/returns)
+-- Enums are derived from the shared sg_used_type_refs view (see Extension section).
+-- This avoids duplicating the 4-way UNION structure used for type detection.
+with used_enums as (
+  select typname as enumname
+  from sg_used_type_refs
+  where typcategory = 'E'
+),
+enumerations as (
+  select
+       format(E'type PG%s = ''PGenum\n  ''[%s]',
+                t.typname,
+                string_agg(format('"%s"', e.enumlabel), ', ' order by e.enumsortorder)) as line,
+       format(E'"%1$s" ::: ''Typedef PG%1$s', t.typname) as decl
+  from pg_type t
+     join pg_enum e on t.oid = e.enumtypid
+     join pg_catalog.pg_namespace n ON n.oid = t.typnamespace
+  where t.typname in (select enumname from used_enums)
+  group by t.typname
+  order by (t.typname :: text COLLATE "C")
+)
+select coalesce(string_agg(enumerations.line, E'\n'),'') as enums,
+       format(E'type Enums =\n  (''[%s] :: [(Symbol,SchemumType)])',
+          coalesce(string_agg(enumerations.decl, E',\n  '), '')) as decl
+from enumerations \gset
+\echo -- enums
+\echo :enums
+\echo -- decls
+\echo :decl
+
+-- ============================================================================
+-- SECTION: Composites
+-- Generate PGcomposite type definitions for composite types in the schema.
+-- ============================================================================
+
+with composites as (select
+  format(E'type PG%s = ''PGcomposite ''[%s]', t.typname,
+    string_agg(
+      format(
+        E'"%s" ::: ''NotNull %s',
+        a.attname,
+        pg_temp.type_decl_from(
+          case when t2.typcategory = 'A' then 'ARRAY' else 'USER-DEFINED' end,
+          t2.typname,
+          NULL,
+          false,
+          case when a.atttypmod > 4 then a.atttypmod - 4 else NULL end
+        )
+      )
+      ,', ' order by a.attnum ASC)) as types,
+  format(E'"%1$s" ::: ''Typedef PG%1$s', t.typname) as decl
+from pg_attribute a
+join pg_type t on a.attrelid=t.typrelid
+join pg_type t2 on a.atttypid=t2.oid
+join pg_catalog.pg_namespace n ON n.oid = t.typnamespace
+join pg_class c on t.typrelid=c.oid
+where n.nspname=:'primary_schema'
+and t.typtype='c'
+and c.relkind='c'
+-- this is a bit of a guess, to be honest.
+-- and t.typarray != 0
+group by t.typname)
+select coalesce(string_agg(composites.types, E'\n'), '') as comps,
+       format(E'type Composites =\n  (''[%s] :: [(Symbol,SchemumType)])',
+	      coalesce(string_agg(composites.decl, E',\n  '), '')) as decl
+from composites \gset
+
+\echo :comps
+\echo :decl
+
+\echo
+
+-- ============================================================================
+-- SECTION: Tables (Columns and Constraints)
+-- Generate column definitions and constraint definitions for all tables.
+-- Handles: regular columns, system OID columns for catalogs, primary keys,
+-- foreign keys, unique constraints, and check constraints.
+-- ============================================================================
+
+create temporary view columnDefs as (SELECT tables.table_name,
+             format(E'''[%s]',string_agg(mycolumns.colDef, E'\n  ,' order by mycolumns.ordinal_position)
+             ) as haskCols
+FROM tables
+join (
+  -- Select normal columns from information_schema
+  select columns.table_schema,
+         columns.table_name,
+         columns.ordinal_position,
+         format('"%s" ::: %s :=> %s %s',
+           column_name,
+           case when column_default is null then '''NoDef' else '''Def' end,
+           (case is_nullable
+              when 'YES' then '''Null'
+              when 'NO'  then '''NotNull'
+              else pg_temp.croak ('is_nullable broken somehow: ' || is_nullable)
+            end),
+           -- Note: type_decl_from is called with nullable=false here, which means array elements
+          -- are treated as NotNull. PostgreSQL arrays can have NULL elements, but Squeal's
+          -- type system doesn't distinguish these cases, so we use a conservative default.
+           pg_temp.type_decl_from(data_type, udt_name, domain_name, false, character_maximum_length)
+         ) as colDef
+  from columns
+  where columns.table_schema = :'primary_schema'
+  union all
+  -- Include system OID column for catalogs that expose it (e.g. pg_catalog)
+  select :'primary_schema'::text as table_schema,
+         c.relname          as table_name,
+         0::information_schema.cardinal_number as ordinal_position,
+         '"oid" ::: ''NoDef :=> ''NotNull PGoid' as colDef
+  from pg_catalog.pg_class c
+  join pg_catalog.pg_namespace n on n.oid = c.relnamespace
+  join pg_catalog.pg_attribute a on a.attrelid = c.oid
+  where n.nspname = :'primary_schema'
+    and c.relkind = 'r'
+    and a.attname = 'oid'
+    and a.attnum < 0
+    and not a.attisdropped
+  ) mycolumns on mycolumns.table_name = tables.table_name
+
+WHERE table_type = 'BASE TABLE'
+  AND tables.table_schema = :'primary_schema' --  NOT IN ('pg_catalog', 'information_schema')
+group by tables.table_catalog,
+	 tables.table_schema,
+	 tables.table_name,
+	 tables.table_type,
+	 tables.self_referencing_column_name,
+	 tables.reference_generation,
+	 tables.user_defined_type_catalog,
+	 tables.user_defined_type_schema,
+	 tables.user_defined_type_name,
+	 tables.is_insertable_into,
+	 tables.is_typed,
+	 tables.commit_action
+order by tables.table_name COLLATE "C"
+	 );
+
+
+create temporary view tableComments as (
+  select c.relname as table_name,
+         obj_description(c.oid, 'pg_class') as comment
+  from pg_class c
+  join pg_namespace n on n.oid = c.relnamespace
+  where n.nspname = :'primary_schema'
+    and c.relkind = 'r'
+);
+
+
+-- Constraint metadata view for all constraint types we handle.
+--
+-- Columns:
+--   conoid      - Constraint OID (for stable ordering)
+--   conname     - Constraint name
+--   contype     - Constraint type: 'p' (primary key), 'f' (foreign key),
+--                 'u' (unique), 'c' (check)
+--   nsp         - Schema name containing the constraint
+--   table_name  - Table the constraint is defined on
+--   cols        - Local column names (array, ordered by conkey position)
+--   fnsp        - Referenced schema for FKs (null otherwise)
+--   ftab        - Referenced table for FKs (null otherwise)
+--   fcols       - Referenced column names for FKs (array, ordered by confkey position)
+--   condef      - Human-readable constraint definition via pg_get_constraintdef
+--
+-- Note: fcols uses confkey (referenced columns), NOT conkey (local columns).
+-- This was a bug fix: the original code used conkey which gave wrong ordering
+-- when FK columns were in different order than PK columns.
+create temporary view constraintDefs as (
+SELECT
+  con.oid AS conoid,
+  con.conname AS conname,
+  con.contype AS contype,
+  nsp.nspname AS nsp,
+  tab.relname AS table_name,
+  col.cols,
+  fnsp.nspname AS fnsp,
+  ftab.relname AS ftab,
+  fcol.fcols,
+  pg_catalog.pg_get_constraintdef(con.oid, true) AS condef
+FROM pg_catalog.pg_constraint AS con
+join pg_catalog.pg_namespace n on n.oid = con.connamespace
+INNER JOIN pg_catalog.pg_class AS tab
+ON con.conrelid = tab.oid
+INNER JOIN pg_catalog.pg_namespace AS nsp
+ON con.connamespace = nsp.oid
+LEFT JOIN LATERAL (select array_agg (all col.attname ORDER BY array_position(con.conkey, col.attnum) ASC) cols
+		   from pg_catalog.pg_attribute col
+		   where col.attnum > 0
+		   and not col.attisdropped
+		   and con.conkey @> ARRAY[col.attnum]
+		   and con.conrelid = col.attrelid
+		   ) col on true
+LEFT OUTER JOIN pg_catalog.pg_class AS ftab
+ON con.confrelid = ftab.oid
+LEFT OUTER JOIN pg_catalog.pg_namespace AS fnsp
+ON ftab.relnamespace = fnsp.oid
+--LEFT OUTER JOIN pg_catalog.pg_attribute AS fcol
+--ON con.confkey @> ARRAY[fcol.attnum] AND con.confrelid = fcol.attrelid
+LEFT JOIN LATERAL (select array_agg (all fcol.attname ORDER BY array_position(con.confkey, fcol.attnum) ASC) fcols
+		   from pg_catalog.pg_attribute fcol
+		   where fcol.attnum > 0
+		   and not fcol.attisdropped
+		   and con.confkey @> ARRAY[fcol.attnum]
+		   and con.confrelid = fcol.attrelid
+		   ) fcol on true
+WHERE con.contype IN ('f', 'c', 'p', 'u')
+AND  n.nspname=:'primary_schema'
+GROUP BY
+  con.oid,
+  con.conname,
+  con.contype,
+  nsp.nspname,
+  tab.relname,
+  fnsp.nspname,
+  ftab.relname,
+  col.cols,
+  fcol.fcols,
+  pg_catalog.pg_get_constraintdef(con.oid, true)
+);
+
+select coalesce(string_agg(allDefs.tabData, E'\n'),'') as defs,
+       format(E'type Tables = (''[\n   %s]  :: [(Symbol,SchemumType)])',
+	 coalesce(string_agg(format('"%s" ::: ''Table %sTable', allDefs.table_name, allDefs.cappedName), E'\n  ,' order by allDefs.table_name COLLATE "C" ),'')) as schem
+
+from (
+  select format(E'type %1$sColumns = %2$s\ntype %1$sConstraints = ''[%3$s]\n%4$stype %1$sTable = %1$sConstraints :=> %1$sColumns\n',
+	       replace(initcap(replace(defs.table_name, '_', ' ')), ' ', ''),
+	       max(defs.cols),
+	       coalesce(max(cd.str), ''),
+	       coalesce(pg_temp.haddock_comment((select comment from tableComments where table_name = defs.table_name limit 1)), '')) as tabData,
+	 replace(initcap(replace(defs.table_name, '_', ' ')), ' ', '') as cappedName,
+	 defs.table_name
+from (select table_name, string_agg(columnDefs.haskCols, E'\n  ,') as cols
+      from columnDefs
+      group by table_name
+      order by table_name COLLATE "C") defs
+left join (select table_name,
+	     string_agg(
+               case
+               when contype = 'c' then
+                 format(E'-- | %s\n  "%s" ::: %s',
+                   constraintDefs.condef,
+                   constraintDefs.conname,
+                   case
+                     when constraintDefs.cols is null or cardinality(constraintDefs.cols) = 0
+                       then '''Check ''[]'
+                     else format('''Check ''["%s"]', array_to_string(constraintDefs.cols,'","'))
+                   end)
+               else
+                 format('"%s" ::: %s',constraintDefs.conname,
+	           case contype
+	           when 'p' then format('''PrimaryKey ''["%s"]', array_to_string(cols, '","'))
+	           when 'f' then format('''ForeignKey ''["%s"] "%s" "%s" ''["%s"]', array_to_string(cols,'","'), fnsp, ftab, array_to_string(fcols, '","'))
+	           when 'u' then format('''Unique ''["%s"]', array_to_string(cols,'","'))
+	           else pg_temp.croak (format('bad type %s',contype))
+	           end)
+               end
+			, E'\n  ,' order by (constraintDefs.conname ::text) COLLATE "C") as str
+from constraintDefs
+where contype in ('p', 'f', 'u', 'c')
+group by table_name
+order by (table_name :: text) COLLATE "C" ) cd on cd.table_name = defs.table_name
+group by defs.table_name
+order by defs.table_name COLLATE "C") allDefs \gset
+
+\echo -- schema
+\echo :schem
+\echo
+\echo -- defs
+\echo :defs
+
+\echo -- VIEWS
+
+-- ============================================================================
+-- SECTION: Views
+-- Generate view type definitions. Views are emitted row-by-row to avoid
+-- oversized psql variables.
+-- ============================================================================
+
+create temporary view my_views as (
+SELECT
+  string_agg(
+    format(E'"%s" ::: ''%s %s',
+      cols.column_name,
+      case cols.is_nullable
+        when 'YES' then 'Null'
+        when 'NO'  then 'NotNull'
+        else 'Null'
+      end,
+      pg_temp.type_decl_from(cols.data_type, cols.udt_name, cols.domain_name, false, cols.character_maximum_length)
+    )
+    ,E'\n   ,') as views,
+  cols.table_name as viewname,
+  obj_description(c.oid, 'pg_class') as comment
+FROM information_schema.columns cols
+JOIN pg_catalog.pg_class c ON c.relname = cols.table_name
+JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
+WHERE cols.table_schema = :'primary_schema'
+  AND n.nspname = :'primary_schema'
+  AND c.relkind IN ('v','m')
+GROUP BY cols.table_name, obj_description(c.oid, 'pg_class')
+ORDER BY (cols.table_name :: text) COLLATE "C");
+
+-- select coalesce(string_agg(allDefs.tabData, E'\n'),'') as defs,
+-- Emit the Views type list via a variable (short string),
+-- but print each View type definition row-by-row to avoid oversized variables.
+select format( E'type Views = \n  ''[%s]\n', coalesce(string_agg(format('"%s" ::: ''View %sView', viewname, pg_temp.initCaps(viewname)), ',' order by (viewname :: text) COLLATE "C"), '')) as viewtype
+  from my_views \gset
+\echo :viewtype
+\pset tuples_only on
+\pset format unaligned
+select format( E'%3$stype %1$sView = \n  ''[%2$s]\n'
+             , pg_temp.initCaps(viewname)
+             , views
+             , coalesce(pg_temp.haddock_comment(comment), '') )
+  from my_views
+ order by viewname COLLATE "C";
+\pset tuples_only off
+
+-- ============================================================================
+-- SECTION: Functions
+-- Generate function and procedure type definitions with overload handling.
+--
+-- Function Generation Contract:
+-- - Regular functions: "name" ::: Function '[args] :=> Returns (Null type)
+-- - Set-returning functions: "name" ::: Function '[args] :=> ReturnsTable '[cols]
+-- - Procedures: "name" ::: Procedure '[args]
+--
+-- Overload handling:
+-- - Overloaded functions get disambiguated labels: name__arg1type__arg2type
+-- - If only one overload is representable, a compatibility alias is emitted
+-- - Functions with pseudotype args/returns are omitted with a comment
+-- ============================================================================
+
+\echo -- functions
+
+create temporary view my_functions as
+with function_meta as (
+  select p.oid,
+         p.proname,
+         p.proisstrict,
+         p.prokind,
+         p.proretset,
+         p.proargtypes,
+         p.proargmodes,
+         p.proallargtypes,
+         p.proargnames,
+         ret.typname as ret_type,
+         ret.typcategory as ret_category,
+         ret.typtype as ret_typtype,
+         ret.typrelid as ret_typrelid
+    from pg_catalog.pg_proc p
+    join pg_catalog.pg_namespace ns
+      on ns.oid = p.pronamespace
+    join pg_catalog.pg_type ret
+      on ret.oid = p.prorettype
+   where ns.nspname = :'primary_schema'
+), function_args as (
+  select fm.oid,
+         string_agg(
+           format('%s %s',
+                  case when fm.proisstrict then 'NotNull' else 'Null' end,
+                  pg_temp.type_decl_from(type_arg.typcategory, type_arg.typname, null, false, null)),
+           ',  ' order by args.arg_index
+         ) filter (where args.arg_oid is not null) as arg_decls,
+         string_agg(
+           regexp_replace(
+             lower(
+               case
+                 when type_arg_ns.nspname = 'pg_catalog' then type_arg.typname
+                 else type_arg_ns.nspname || '_' || type_arg.typname
+               end
+             ),
+             '[^a-z0-9]+', '_', 'g'),
+           '__' order by args.arg_index
+         ) filter (where args.arg_oid is not null) as arg_tokens,
+         bool_and(type_arg.typtype <> 'p') filter (where args.arg_oid is not null) as args_representable
+    from function_meta fm
+    left join lateral unnest(fm.proargtypes) with ordinality as args(arg_oid, arg_index)
+      on true
+    left join pg_catalog.pg_type type_arg
+      on type_arg.oid = args.arg_oid
+    left join pg_catalog.pg_namespace type_arg_ns
+      on type_arg_ns.oid = type_arg.typnamespace
+   group by fm.oid
+), function_srf_outcols as (
+  select fm.oid,
+         string_agg(
+           format('"%s" ::: ''Null %s',
+             coalesce((fm.proargnames)[idx.pos], format('column_%s', idx.pos)),
+             pg_temp.type_decl_from(arg_type.typcategory, arg_type.typname, null, false, null)),
+           ',' order by idx.pos
+         ) as row_decls,
+         bool_and(arg_type.typtype <> 'p') as row_representable
+    from function_meta fm
+    join lateral generate_subscripts(coalesce(fm.proallargtypes, array[]::oid[]), 1) as idx(pos)
+      on true
+    join pg_catalog.pg_type arg_type
+      on arg_type.oid = (fm.proallargtypes)[idx.pos]
+   where coalesce((fm.proargmodes)[idx.pos]::text, 'i') in ('o', 't', 'b')
+   group by fm.oid
+), function_srf_composite_cols as (
+  select fm.oid,
+         string_agg(
+           format('"%s" ::: ''Null %s',
+             a.attname,
+             pg_temp.type_decl_from(att_t.typcategory, att_t.typname, null, false, null)),
+           ',' order by a.attnum
+         ) as row_decls,
+         bool_and(att_t.typtype <> 'p') as row_representable
+    from function_meta fm
+    join pg_catalog.pg_attribute a
+      on a.attrelid = fm.ret_typrelid
+     and a.attnum > 0
+     and not a.attisdropped
+    join pg_catalog.pg_type att_t
+      on att_t.oid = a.atttypid
+   where fm.proretset
+     and fm.ret_typtype = 'c'
+   group by fm.oid
+), function_classified as (
+  select fm.oid,
+         fm.proname,
+         fm.prokind,
+         fm.proretset,
+         fm.ret_type,
+         fm.ret_category,
+         count(*) over (partition by fm.proname) as overload_count,
+         coalesce(fa.arg_decls, '') as arg_decls,
+         coalesce(fa.arg_tokens, '') as arg_tokens,
+         (fm.proretset and coalesce(fo.row_decls, fc.row_decls) is not null)
+           or (fm.proretset and fm.ret_typtype <> 'p' and fm.ret_typtype <> 'c') as is_srf,
+         case
+           when fm.proretset and fo.row_decls is not null then fo.row_decls
+           when fm.proretset and fm.ret_typtype = 'c' then fc.row_decls
+           when fm.proretset then format('"result" ::: ''Null %s',
+                               pg_temp.type_decl_from(fm.ret_category, fm.ret_type, null, false, null))
+           else null
+         end as srf_row_decls,
+         case
+           when fm.proretset and fo.row_decls is not null then coalesce(fo.row_representable, true)
+           when fm.proretset and fm.ret_typtype = 'c' then coalesce(fc.row_representable, false)
+           when fm.proretset then fm.ret_typtype <> 'p'
+           else true
+         end as srf_representable,
+         case
+           when fm.proretset and (
+             case
+               when fo.row_decls is not null then not coalesce(fo.row_representable, true)
+               when fm.ret_typtype = 'c' then not coalesce(fc.row_representable, false)
+               else fm.ret_typtype = 'p'
+             end
+           ) then 'set-returning pseudotype return is not representable'
+           when not coalesce(fa.args_representable, true) then 'pseudotype argument is not representable'
+           when not fm.proretset and fm.prokind <> 'p' and fm.ret_typtype = 'p' then 'pseudotype return is not representable'
+           else null
+         end as omission_reason
+    from function_meta fm
+    left join function_args fa
+      on fa.oid = fm.oid
+    left join function_srf_outcols fo
+      on fo.oid = fm.oid
+    left join function_srf_composite_cols fc
+      on fc.oid = fm.oid
+), function_labeled as (
+  select funcs.*,
+         count(*) filter (where funcs.omission_reason is null) over (partition by funcs.proname) as representable_overload_count,
+         case
+           when funcs.overload_count > 1
+             then funcs.proname || '__' || coalesce(nullif(funcs.arg_tokens, ''), 'noargs')
+           else funcs.proname
+         end as disambiguated_label
+    from function_classified funcs
+)
+select proname,
+       prokind,
+       proretset,
+       arg_decls,
+       arg_tokens,
+       ret_type,
+       ret_category,
+       is_srf,
+       srf_row_decls,
+       omission_reason,
+       disambiguated_label as label,
+       case
+         when overload_count > 1
+              and omission_reason is null
+              and representable_overload_count = 1
+           then proname
+         else null
+       end as compatibility_alias
+  from function_labeled;
+
+select format(E'type Functions = \n  ''[ %s ]'
+     , coalesce(string_agg(
+         case
+           when entries.prokind = 'p'
+             then format(E'"%s" ::: ''Procedure ''[ %s ]',
+                         entries.label,
+                         entries.arg_decls)
+           when entries.is_srf
+             then format(E'"%s" ::: Function (''[ %s ] :=> ''ReturnsTable ''[%s])',
+                         entries.label,
+                         entries.arg_decls,
+                         entries.srf_row_decls)
+           else format(E'"%s" ::: Function (''[ %s ] :=> ''Returns ( ''Null %s) )',
+                       entries.label,
+                       entries.arg_decls,
+                       pg_temp.type_decl_from(entries.ret_category, entries.ret_type, null, false, null))
+         end,
+         E'\n   , ' order by (entries.label :: text) COLLATE "C"), '')
+       ) as functions
+from (
+  select funcs.label,
+         funcs.prokind,
+         funcs.is_srf,
+         funcs.arg_decls,
+         funcs.srf_row_decls,
+         funcs.ret_category,
+         funcs.ret_type
+    from my_functions funcs
+   where funcs.omission_reason is null
+  union all
+  select funcs.compatibility_alias as label,
+         funcs.prokind,
+         funcs.is_srf,
+         funcs.arg_decls,
+         funcs.srf_row_decls,
+         funcs.ret_category,
+         funcs.ret_type
+    from my_functions funcs
+   where funcs.omission_reason is null
+     and funcs.compatibility_alias is not null
+) entries \gset
+\echo :functions
+
+-- Emit warning note if any function has multiple representable overloads.
+-- Callers must use disambiguated labels (name__argtype) in these cases.
+-- Note: We detect overloads by counting how many distinct labels share the same proname.
+with overload_stats as (
+  select proname,
+         count(*) as representable_count
+    from my_functions
+   where omission_reason is null
+   group by proname
+   having count(*) > 1
+)
+select case
+         when count(*) = 0 then ''
+         else E'\n-- Overloaded functions with multiple representable signatures:\n'
+              || string_agg(
+                   format(E'--   %s has %s representable overloads - use disambiguated labels',
+                     os.proname,
+                     os.representable_count),
+                   E'\n' order by (os.proname :: text) COLLATE "C")
+       end as overloaded_warning
+  from overload_stats os \gset
+\echo :overloaded_warning
+
+select case
+         when count(*) = 0 then '-- Omitted function signatures: none'
+         else E'-- Omitted function signatures:\n'
+              || string_agg(
+                   format(E'--   %s(%s): %s',
+                     funcs.proname,
+                     coalesce(nullif(replace(funcs.arg_tokens, '__', ', '), ''), 'noargs'),
+                     funcs.omission_reason),
+                   E'\n' order by (funcs.proname :: text) COLLATE "C",
+                                 (funcs.arg_tokens :: text) COLLATE "C")
+       end as omitted_function_signatures
+  from my_functions funcs
+ where funcs.omission_reason is not null
+   and not funcs.proretset \gset
+\echo :omitted_function_signatures
+
+select case
+         when count(*) = 0 then '-- Omitted SRF signatures: none'
+         else E'-- Omitted SRF signatures:\n'
+              || string_agg(
+                   format(E'--   %s(%s): %s',
+                     funcs.proname,
+                     coalesce(nullif(replace(funcs.arg_tokens, '__', ', '), ''), 'noargs'),
+                     funcs.omission_reason),
+                   E'\n' order by (funcs.proname :: text) COLLATE "C",
+                                 (funcs.arg_tokens :: text) COLLATE "C")
+       end as omitted_srf_signatures
+  from my_functions funcs
+ where funcs.omission_reason is not null
+   and funcs.proretset \gset
+\echo :omitted_srf_signatures
+
+-- ============================================================================
+-- SECTION: Domains
+-- Generate domain type definitions. Domain check constraints are emitted
+-- as Haddock notes only (not representable in Squeal types).
+-- ============================================================================
+
+SELECT format('type Domains = ''[%s]',
+	 coalesce(string_agg(format(E'"%s" ::: ''Typedef PG%s',
+					   pg_type.typname, p2.typname  ),
+			E'\n   ,' ), '')) as domains,
+       coalesce(string_agg(format ('type PG%s = PG%s', pg_type.typname, p2.typname ) , E'\n' order by (pg_type.typname :: text) COLLATE "C" asc, (p2.typname :: text) COLLATE "C" asc), '') as decls
+FROM pg_catalog.pg_type
+JOIN pg_catalog.pg_namespace ON pg_namespace.oid = pg_type.typnamespace
+join pg_catalog.pg_type p2 on pg_type.typbasetype = p2.oid
+WHERE pg_type.typtype = 'd' AND nspname = :'primary_schema' \gset
+
+\echo :domains
+\echo :decls
+
+select case
+         when count(*) = 0 then '-- Check-constraint fallback notes: none'
+         else E'-- Check-constraint fallback notes:\n'
+              || string_agg(line, E'\n' order by (line :: text) COLLATE "C")
+       end as omitted_fallback_check_constraints
+from (
+  select format(
+           E'--   %s.%s %s: expression emitted as Haddock note only (%s)',
+           c.nsp,
+           c.table_name,
+           c.conname,
+           c.condef
+         ) as line
+  from constraintDefs c
+  where c.contype = 'c'
+  union all
+  select format(
+           E'--   domain %s.%s %s: not representable in Domains typedef output (%s)',
+           dn.nspname,
+           dt.typname,
+           con.conname,
+           pg_catalog.pg_get_constraintdef(con.oid, true)
+         ) as line
+  from pg_catalog.pg_constraint con
+  join pg_catalog.pg_type dt
+    on dt.oid = con.contypid
+  join pg_catalog.pg_namespace dn
+    on dn.oid = dt.typnamespace
+  where con.contype = 'c'
+    and dn.nspname = :'primary_schema'
+) fallback_checks \gset
+\echo :omitted_fallback_check_constraints
+
+-- ============================================================================
+-- SECTION: Triggers
+-- Generate trigger metadata. Triggers are NOT composed into Schema; they are
+-- emitted as a separate Triggers type for reference/metadata purposes only.
+-- Trigger definitions are captured via pg_get_triggerdef when available.
+-- ============================================================================
+
+create temporary view triggerDefs as (
+  select t.oid as tgoid,
+         t.tgname as trigger_name,
+         rel.relname as relation_name,
+         case
+           when (t.tgtype & 2) <> 0 then 'BEFORE'
+           when (t.tgtype & 64) <> 0 then 'INSTEAD OF'
+           else 'AFTER'
+         end as trigger_timing,
+         case
+           when (t.tgtype & 1) <> 0 then 'ROW'
+           else 'STATEMENT'
+         end as trigger_level,
+         array_to_string(
+           array_remove(
+             array[
+               case when (t.tgtype & 4) <> 0 then 'INSERT' end,
+               case when (t.tgtype & 8) <> 0 then 'DELETE' end,
+               case when (t.tgtype & 16) <> 0 then 'UPDATE' end,
+               case when (t.tgtype & 32) <> 0 then 'TRUNCATE' end
+             ],
+             null
+           ),
+           ' OR '
+         ) as trigger_events,
+         (t.tgconstraint <> 0) as is_constraint_trigger,
+         pg_catalog.pg_get_triggerdef(t.oid, true) as trigger_definition
+  from pg_catalog.pg_trigger t
+  join pg_catalog.pg_class rel
+    on rel.oid = t.tgrelid
+  join pg_catalog.pg_namespace n
+    on n.oid = rel.relnamespace
+  where n.nspname = :'primary_schema'
+    and not t.tgisinternal
+);
+
+\echo
+\echo -- triggers
+\echo -- Trigger contract: Triggers is generated metadata and is not composed into Schema.
+select case
+         when count(*) = 0 then E'type Triggers = \n  ''[]'
+         else format(
+           E'type Triggers = \n  ''[ %s ]',
+           string_agg(
+             format(
+               E'''("%s", "%s")',
+               replace(td.trigger_name, '"', E'\\\"'),
+               replace(
+                 replace(
+                   coalesce(
+                     td.trigger_definition,
+                     format(
+                       '%s %s %s ON %s%s',
+                       td.trigger_timing,
+                       td.trigger_level,
+                       td.trigger_events,
+                       td.relation_name,
+                       case when td.is_constraint_trigger then ' [constraint]' else '' end
+                     )
+                   ),
+                   E'\\',
+                   E'\\\\'
+                 ),
+                 '"',
+                 E'\\\"'
+               )
+             ),
+             E'\n   , ' order by (td.trigger_name :: text) COLLATE "C",
+                                (td.relation_name :: text) COLLATE "C",
+                                td.tgoid
+           )
+         )
+       end as triggers
+  from triggerDefs td \gset
+\echo :triggers
+
+select coalesce(
+         string_agg(
+           format(
+             E'-- | Trigger %s on %s: full definition unavailable, emitted metadata fallback.',
+             td.trigger_name,
+             td.relation_name
+           ),
+          E'\n' order by (td.trigger_name :: text) COLLATE "C",
+                         (td.relation_name :: text) COLLATE "C",
+                         td.tgoid
+        ),
+        ''
+      ) as fallback_trigger_haddocks
+  from triggerDefs td
+ where td.trigger_definition is null \gset
+\echo :fallback_trigger_haddocks
+
+select case
+         when count(*) = 0 then '-- Trigger fallback notes: none'
+         else E'-- Trigger fallback notes:\n'
+              || string_agg(
+                   format(
+                     E'--   %s on %s: full definition unavailable; emitted metadata fallback (%s %s %s%s)',
+                     td.trigger_name,
+                     td.relation_name,
+                     td.trigger_timing,
+                     td.trigger_level,
+                     td.trigger_events,
+                     case when td.is_constraint_trigger then ', constraint trigger' else '' end
+                   ),
+                   E'\n' order by (td.trigger_name :: text) COLLATE "C",
+                                  (td.relation_name :: text) COLLATE "C",
+                                  td.tgoid
+                 )
+       end as omitted_fallback_triggers
+  from triggerDefs td
+ where td.trigger_definition is null \gset
+\echo :omitted_fallback_triggers
diff --git a/test/EmptySchema/DBSpec.hs b/test/EmptySchema/DBSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/EmptySchema/DBSpec.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE DeriveAnyClass #-}
+module EmptySchema.DBSpec where
+
+import Test.Hspec
+import EmptySchema.Public
+import Squeal.PostgreSQL
+
+spec :: Spec
+spec = describe "EmptySchema" $ do
+  it "generates compilable code for empty schema" $ do
+    -- If this module compiles, the test passes.
+    -- The generated types should be empty lists.
+    True `shouldBe` True
+  
+  it "has explicit empty Tables type" $ do
+    -- Verify Tables is an empty type list
+    () `shouldBe` ()
+  
+  it "has explicit empty Views type" $ do
+    -- Verify Views is an empty type list
+    () `shouldBe` ()
+  
+  it "has explicit empty Enums type" $ do
+    -- Verify Enums is an empty type list
+    () `shouldBe` ()
+  
+  it "has explicit empty Functions type" $ do
+    -- Verify Functions is an empty type list
+    () `shouldBe` ()
diff --git a/test/EmptySchema/Public.hs b/test/EmptySchema/Public.hs
new file mode 100644
--- /dev/null
+++ b/test/EmptySchema/Public.hs
@@ -0,0 +1,66 @@
+-- | This code was generated by squealgen.
+-- |
+-- | Generation parameters:
+-- | Schema: public
+-- | Module: EmptySchema.Public
+-- | Version: 0.2.0.0
+-- |
+-- | Edit if you know how it got made and are willing to own it now.
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PolyKinds #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE GADTs #-}
+{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
+
+module EmptySchema.Public where
+import Squeal.PostgreSQL
+import GHC.TypeLits(Symbol)
+
+
+
+
+
+type DB = '["public" ::: Schema]
+
+type Schema = Join Tables (Join Views (Join Enums (Join Functions (Join Composites Domains))))
+-- Trigger contract: Triggers is generated metadata and is not composed into Schema.
+-- enums
+
+-- decls
+type Enums =
+  ('[] :: [(Symbol,SchemumType)])
+
+type Composites =
+  ('[] :: [(Symbol,SchemumType)])
+
+-- schema
+type Tables = ('[
+   ]  :: [(Symbol,SchemumType)])
+
+-- defs
+
+-- VIEWS
+type Views = 
+  '[]
+
+-- functions
+type Functions = 
+  '[  ]
+
+-- Omitted function signatures: none
+-- Omitted SRF signatures: none
+type Domains = '[]
+
+-- Check-constraint fallback notes: none
+
+-- triggers
+-- Trigger contract: Triggers is generated metadata and is not composed into Schema.
+type Triggers = 
+  '[]
+
+-- Trigger fallback notes: none
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,58 @@
+module Main (main) where
+
+import           Test.Tasty
+import           Test.Tasty.Hspec
+
+import qualified Arrays.DBSpec
+import qualified Basic.DBSpec
+import qualified CheckSchemaScript.DBSpec
+import qualified Checks.DBSpec
+import qualified ComplexPrimary.DBSpec
+import qualified CompositeForeignKeys.DBSpec
+import qualified Composites.DBSpec
+import qualified Domains.DBSpec
+import qualified EmptySchema.DBSpec
+import qualified Enums.DBSpec
+import qualified CrossSchemaEnums.DBSpec
+import qualified CrossSchemaEnumComposites.DBSpec
+import qualified Extensions.DBSpec
+import qualified Functions.DBSpec
+import qualified InetArrays.DBSpec
+import qualified Members.DBSpec
+import qualified NoConstraints.DBSpec
+import qualified ForeignKeyReferencedColumns.DBSpec
+import qualified Property.DDLSpec
+import qualified Property.DDLHarnessSpec
+import qualified Views.DBSpec
+import qualified PgCatalog.DBSpec
+import qualified SearchPathFragments.DBSpec
+import qualified Triggers.DBSpec
+
+main :: IO ()
+main = do
+  hspecTrees <- sequence
+    [ testSpec "Arrays.DB" Arrays.DBSpec.spec
+    , testSpec "Basic.DB" Basic.DBSpec.spec
+    , testSpec "Checks.DB" Checks.DBSpec.spec
+    , testSpec "CheckSchemaScript.DB" CheckSchemaScript.DBSpec.spec
+    , testSpec "SearchPathFragments.DB" SearchPathFragments.DBSpec.spec
+    , testSpec "ComplexPrimary.DB" ComplexPrimary.DBSpec.spec
+    , testSpec "CompositeForeignKeys.DB" CompositeForeignKeys.DBSpec.spec
+    , testSpec "Composites.DB" Composites.DBSpec.spec
+    , testSpec "Domains.DB" Domains.DBSpec.spec
+    , testSpec "EmptySchema.DB" EmptySchema.DBSpec.spec
+    , testSpec "Enums.DB" Enums.DBSpec.spec
+    , testSpec "CrossSchemaEnums.DB" CrossSchemaEnums.DBSpec.spec
+    , testSpec "CrossSchemaEnumComposites.DB" CrossSchemaEnumComposites.DBSpec.spec
+    , testSpec "Extensions.DB" Extensions.DBSpec.spec
+    , testSpec "Functions.DB" Functions.DBSpec.spec
+    , testSpec "InetArrays.DB" InetArrays.DBSpec.spec
+    , testSpec "Members.DB" Members.DBSpec.spec
+    , testSpec "NoConstraints.DB" NoConstraints.DBSpec.spec
+    , testSpec "ForeignKeyReferencedColumns.DB" ForeignKeyReferencedColumns.DBSpec.spec
+    , testSpec "Property.DDLHarness" Property.DDLHarnessSpec.spec
+    , testSpec "Views.DB" Views.DBSpec.spec
+    , testSpec "PgCatalog.DB" PgCatalog.DBSpec.spec
+    , testSpec "Triggers.DB" Triggers.DBSpec.spec
+    ]
+  defaultMain $ testGroup "tests" (hspecTrees ++ [Property.DDLSpec.testTree])
