{"id":20704148,"url":"https://github.com/solidsnack/pg-sql-variants","last_synced_at":"2025-12-24T06:23:20.607Z","repository":{"id":66073487,"uuid":"53839670","full_name":"solidsnack/pg-sql-variants","owner":"solidsnack","description":"Variants types for PostgreSQL","archived":false,"fork":false,"pushed_at":"2020-04-21T23:51:37.000Z","size":11,"stargazers_count":30,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-05T17:08:17.115Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PLpgSQL","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/solidsnack.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-03-14T08:37:51.000Z","updated_at":"2024-08-25T21:02:33.000Z","dependencies_parsed_at":null,"dependency_job_id":"4df65de2-8de0-4aa9-9846-ded083f08b07","html_url":"https://github.com/solidsnack/pg-sql-variants","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/solidsnack%2Fpg-sql-variants","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/solidsnack%2Fpg-sql-variants/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/solidsnack%2Fpg-sql-variants/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/solidsnack%2Fpg-sql-variants/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/solidsnack","download_url":"https://codeload.github.com/solidsnack/pg-sql-variants/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":242973962,"owners_count":20215245,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-17T01:11:11.784Z","updated_at":"2025-12-24T06:23:20.589Z","avatar_url":"https://github.com/solidsnack.png","language":"PLpgSQL","funding_links":[],"categories":[],"sub_categories":[],"readme":"How To Use This Repo\n====================\n\nThis repo provides utilities to for modeling variant types in Postgres and\nprovides a demo schema, as well. To try it out, first use Git to obtain the\nrelevant code:\n\n```bash\n:;  git clone git@github.com:solidsnack/pg-sql-variants.git\n:;  cd pg-sql-variants/\n:;  git submodule update --init --recursive\n```\n\nThen load the utilities and the sample schema in Postgres:\n\n```sql\n:;  psql\nLine style is unicode.\nExpanded display is used automatically.\nNull display is \"\\N\".\nTiming is on.\npsql (12.1)\nType \"help\" for help.\n\n--# thelyfsoshort@[local]/~\n\\i init.psql \nBEGIN\n...\nCOMMIT\n...\nBEGIN\n...\nCOMMIT\n...\nBEGIN\n...\nCOMMIT\n...\n```\n\nThe sample schema helps us to demonstrate a simple polymorhpic datatype: an\n`animal` type with concrete `cat`, `dog` and `walrus` subtypes.\n\n```sql\n--# thelyfsoshort@[local]/~\nSELECT tablename FROM pg_tables WHERE schemaname = 'inetorg';\n tablename\n───────────\n cat\n walrus\n dog\n animal\n(4 rows)\n```\n\nLet's setup the variant relationship between the types in the `inetorg`\nnamespace with the `variant()` function from the `variants` namespace:\n\n```sql\n--# thelyfsoshort@[local]/~\nSET search_path TO inetorg, variants, \"$user\", public;\n\n--# thelyfsoshort@[local]/~\nSELECT * FROM variant('animal', 'cat');\nSELECT * FROM variant('animal', 'walrus');\nSELECT * FROM variant('animal', 'dog');\n```\n\nWe can see that there are no `animal`s and there are no `cat`s:\n\n```sql\n--# thelyfsoshort@[local]/~\nSELECT * FROM animal;\n ident \n───────\n(0 rows)\n\n--# thelyfsoshort@[local]/~\nSELECT * FROM cat;\n license │ responds_to │ doglike\n─────────┼─────────────┼─────────\n(0 rows)\n```\n\nThe `variants.variant()` function is basically a SQL macro; it sets up several\ntriggers every time it is called. Let's add a `cat`:\n\n```sql\n--# thelyfsoshort@[local]/~ \nINSERT INTO cat VALUES ('00000000-0000-0000-0000-000000000001', 'felix', FALSE);\nINSERT 0 1\n```\n\nThe triggers ensure that records are added to the `animal` table, as well.\n\n```sql\n--# thelyfsoshort@[local]/~\nSELECT * FROM cat;\n               license                │ responds_to │ doglike\n──────────────────────────────────────┼─────────────┼─────────\n 00000000-0000-0000-0000-000000000001 │ felix       │ f\n(1 row)\n\n--# thelyfsoshort@[local]/~\nSELECT * FROM animal;\n                ident                 \n──────────────────────────────────────\n 00000000-0000-0000-0000-000000000001\n(1 row)\n```\n\nIn addition to the triggers, `variants.variant()` also maintains a join table,\nwith one column for each variant type. In this case, the join table is named\n`animal*`:\n\n```sql\n--# thelyfsoshort@[local]/~\nSELECT * FROM \"animal*\";\nTime: 0.285 ms\n─[ RECORD 1 ]──────────────────────────────────────────\nident  │ 00000000-0000-0000-0000-000000000001\ntype   │ cat\ncat    │ (00000000-0000-0000-0000-000000000001,felix,f)\nwalrus │ \\N\ndog    │ \\N\n(1 row)\n```\n\nThe join table illustrates a cool Postgres features: columns with row types.\n\nWhat good is a `cat`? Better delete while we're not sure:\n\n```sql\n--# thelyfsoshort@[local]/~\nDELETE FROM cat;\nDELETE 1\n```\n\nNo more `cat`s, no more `animal*`s:\n\n```sql\n--# thelyfsoshort@[local]/~\nSELECT * FROM cat;\n license │ responds_to │ doglike\n─────────┼─────────────┼─────────\n(0 rows)\n\n--# thelyfsoshort@[local]/~\nSELECT * FROM \"animal*\";\n ident │ type │ cat │ walrus │ dog\n───────┼──────┼─────┼────────┼─────\n(0 rows)\n```\n\nOur Approach to Variant Types in Postgres\n=========================================\n\nTyped variants, case classes, tagged unions, algebraic data types or\njust [enums]: variant types are a feature common to many programming languages\nbut are an awkward fit for SQL.\n\n[enums]: https://doc.rust-lang.org/book/enums.html\n\nThe fundamental difficulty is that foreign keys can reference columns of only\none other table. By combining `VIEW`s, triggers and Postgres's JSON data-type,\nwe can group related types like `cat` and `walrus` under a tagged union like\n`animal`, allowing other tables to create foreign keys that reference\n`animal`.\n\n```sql\nCREATE TABLE cat (\n  license       uuid PRIMARY KEY,\n  responds_to   text NOT NULL,\n  doglike       boolean DEFAULT TRUE\n);\n\nCREATE TABLE walrus (\n  registration  uuid PRIMARY KEY,\n  nickname      text,\n  size          text NOT NULL DEFAULT 'big' CHECK (size IN ('small', 'big')),\n  haz_bucket    boolean NOT NULL DEFAULT FALSE\n);\n\nCREATE TABLE animal (\n  ident         uuid PRIMARY KEY\n);\n```\n\nThe process for forming the foreign key and triggers is completely formulaic\nand we capture it in a stored procedure, `variant` (in `variants.sql`) that\nallows one to put `cat` and `walrus` together under `animal`:\n\n```sql\nSELECT * FROM variant('animal', 'cat');\nSELECT * FROM variant('animal', 'walrus');\n```\n\nChanges to keys are propagated bidirectionally between `animal` and its\nvariants. A `DELETE` against a cat's UUID in `animal` will remove the row from\n`cat`; and a delete against `cat` will remove the row from `animal`.\n\n\nWhy data in your database is not like data in your app\n------------------------------------------------------\n\nImagine for a moment the data loaded in your app. There are `Cat`s and\n`Walrus`es of class `Animal`; there are `String`s, `Integer`s,\n`StructTime`s... But how would you go about searching and sorting these\nobjects? One could say this is a bad question with a bad answer.\n\nIt's a bad question because most of the time we have the objects we need ready\nto hand, assigned to variables in the right place in our program -- we don't\nneed to find them. We don't ever sort \"all\" integers, just the relevant ones.\n\nThe answer is bad because one would search and sort all the objects of a given\ntype by walking the heap. This might be facilitated by the runtime (Ruby's\n`ObjectSpace.each_object(\u003ccls\u003e)` comes to mind) or it might not; but one is in\nfor a linear scan either way; and there is potential for conflict with other\nthreads of execution -- either preventing them from running, or tripping over\ninconsistencies they introduce.\n\nIn a database, however, we do not rely on having the right context to find an\nobject. Whereas in a programming context we use the objects to get the fields,\nin a storage context we use the fields to find the objects; there is no notion\nof identity apart from field values. This is the heart of the\nobject-relational (or struct-enum-relational or ADT-relational) mismatch.\n\nThe two models overlap when we consider global, concurrent data structures\nlike event buses or concurrent maps. In SQL terms, each concurrent map would\nbe a relation, and in SQL each relation is a distinct type. A database is what\nyou would get if each of the types in your language were automatically\nassociated with a concurrent map.\n\nThere are two abstractions relating to types which become strange in this\nall-types-backed-with-maps model:\n\n* Inheritance, abstract base classes, and traits\n* Generics (in the Java sense) or templates (in the C++ sense)\n\nWith regards to inheritance, one wonders what it would mean to insert an\n`orange` in the `fruit` table or the `citrus` table. Clearly, inserting it in\nany one of them should insert it in all of them. It is an ambiguity that gives\nthe author pause.\n\nWith regards to templated definitions, it stands to reason that these can have\nno \"live\" representation in the database. Tables are there, or they aren't.\nPerhaps a database's SQL dialect could support template expansion; but this\nfeature would have no impact on the nature of queries or relationships between\ntables.\n\n\nHow tagged unions can help\n--------------------------\n\nTyped variants -- or tagged unions -- are a minimal way to expand SQL's\nsupport for polymorphism that is helpful to object-oriented languages, and\nlanguages like Haskell, Rust and Go which provide products or sums of products\nas well as traits.\n\nIn our approach, the types which are part of the union are all themselves\nphysical tables with primary keys that are type compatible. We create a new\ntable for the union, the only columns of which are the columns of the primary\nkey and, through triggers, we ensure that inserts, updates and deletes to any\nof the variant tables are also propagated to the union.\n\nThe union table ensures that the key spaces of the variants are disjoint and\nallows for other tables to declare foreign keys that references the union.\nNormal database validation logic takes over from there. The alternative would\nbe to have constraint triggers on each client table for each table in the\nvariant and to have triggers on each variant table for each client. This would\nboth be less expressive and a likely source of errors.\n\nSQL tagged unions in this style support \"composition\" instead of inheritance\nfor polymorphism. For example, in the case where we have a type of letters and\nwould like be able handle Swiss letter, Spanish letter, Egyptian letter and\nmore, the modeller is tasked with breaking out the common fields into a\n`letter` table which would reference `national_variant` which is a union of\n`egyptian`, `ethiopian`, `etruscan` and so forth.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsolidsnack%2Fpg-sql-variants","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsolidsnack%2Fpg-sql-variants","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsolidsnack%2Fpg-sql-variants/lists"}