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 +71/−0
- LICENSE +30/−0
- README.md +256/−0
- Setup.hs +2/−0
- app/Main.hs +55/−0
- squealgen +1094/−0
- squealgen.cabal +82/−0
- squealgen.sql +1079/−0
- test/EmptySchema/DBSpec.hs +32/−0
- test/EmptySchema/Public.hs +66/−0
- test/Spec.hs +58/−0
+ 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.++++## 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])