packages feed

squealgen (empty) → 0.2.0.0

raw patch · 11 files changed

+2825/−0 lines, 11 filesdep +basedep +bytestringdep +containerssetup-changed

Dependencies added: base, bytestring, containers, directory, falsify, filepath, generics-sop, hspec, hspec-expectations-lifted, iproute, mtl, network-ip, postgresql-binary, process, squeal-postgresql, tasty, tasty-hspec, temporary, text, tmp-postgres, unliftio

Files

+ ChangeLog.md view
@@ -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.
+ LICENSE view
@@ -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.
+ README.md view
@@ -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.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -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
+ squealgen view
@@ -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
+ squealgen.cabal view
@@ -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
+ squealgen.sql view
@@ -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
+ test/EmptySchema/DBSpec.hs view
@@ -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` ()
+ test/EmptySchema/Public.hs view
@@ -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
+ test/Spec.hs view
@@ -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])