{"id":21997366,"url":"https://github.com/stephane-klein/postgres-tags-model-poc","last_synced_at":"2026-04-13T23:33:35.578Z","repository":{"id":172561385,"uuid":"647473478","full_name":"stephane-klein/postgres-tags-model-poc","owner":"stephane-klein","description":"Database model POC based on Tags and Postgres Arrays, a Purrrfect Combination article.","archived":false,"fork":false,"pushed_at":"2024-08-14T07:32:50.000Z","size":43,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-23T04:44:20.356Z","etag":null,"topics":["pgtap","postgresql","proof-of-concept","sql","tagging"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","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/stephane-klein.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":"2023-05-30T21:29:50.000Z","updated_at":"2024-08-14T07:32:54.000Z","dependencies_parsed_at":null,"dependency_job_id":"20b58748-9b01-47d5-8ccc-14f3a018b2ea","html_url":"https://github.com/stephane-klein/postgres-tags-model-poc","commit_stats":null,"previous_names":["stephane-klein/postgres-tags-model-poc"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/stephane-klein/postgres-tags-model-poc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stephane-klein%2Fpostgres-tags-model-poc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stephane-klein%2Fpostgres-tags-model-poc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stephane-klein%2Fpostgres-tags-model-poc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stephane-klein%2Fpostgres-tags-model-poc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stephane-klein","download_url":"https://codeload.github.com/stephane-klein/postgres-tags-model-poc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stephane-klein%2Fpostgres-tags-model-poc/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31775917,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-13T20:17:16.280Z","status":"ssl_error","status_checked_at":"2026-04-13T20:17:08.216Z","response_time":93,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["pgtap","postgresql","proof-of-concept","sql","tagging"],"created_at":"2024-11-29T22:16:00.908Z","updated_at":"2026-04-13T23:33:35.540Z","avatar_url":"https://github.com/stephane-klein.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"Database model POC based on [Tags and Postgres Arrays, a Purrrfect Combination](https://www.crunchydata.com/blog/tags-aand-postgres-arrays-a-purrfect-combination) article.\n\nThe [`query-string-js-parser/`](./query-string-js-parser/) folder contains a Javascript parser that converts a string query into sql.\n\n```sh\n$ docker compose up -d --wait\n```\n\n```\n$ ./scripts/seed.sh\n$ ./scripts/fixtures.sh\n```\n\nEnter in Postgres:\n```\n./scripts/enter-in-pg.sh\npostgres=#\n```\n\nRetrieves all contacts with their associated tag names.\n\n```sql\npostgres=# SELECT * FROM main.contacts_with_tag_names;\n+--------------------------------------+-------+--------+------------------+\n| id                                   | name  | tags   | tag_names        |\n|--------------------------------------+-------+--------+------------------|\n| 0c6f17dd-03a3-484d-b4fb-619fcd9cd4f7 | User1 | [1, 2] | ['tag1', 'tag2'] |\n| a270741b-6a1a-4a2f-a847-eb37c522f596 | User2 | [2, 3] | ['tag2', 'tag3'] |\n| 019d3463-6294-4f30-af96-f3e8d698cd1d | User4 | \u003cnull\u003e | [None]           |\n| 18a86616-a1f5-41b2-89ff-4dd51d132e58 | User3 | [4, 5] | ['tag4', 'tag5'] |\n+--------------------------------------+-------+--------+------------------+\n```\n\nRetrieves all contacts with the id and name of their associated tags in json format.\n\n```sql\npostgres=# SELECT * FROM main.contacts_with_tags;\n+--------------------------------------+-------+--------+------------------------------------------------------------+\n| id                                   | name  | tags   | json_agg                                                   |\n|--------------------------------------+-------+--------+------------------------------------------------------------|\n| 4f026b11-e7c5-4143-b9f6-4105f79e17ee | User4 | \u003cnull\u003e | [{\"id\" : null, \"name\" : null}]                             |\n| ae497426-f391-403b-a902-871f35fada89 | User1 | [1, 2] | [{\"id\" : 1, \"name\" : \"tag1\"}, {\"id\" : 2, \"name\" : \"tag2\"}] |\n| 343315b7-94b3-4ef3-975a-da49005b29b8 | User3 | [4, 5] | [{\"id\" : 4, \"name\" : \"tag4\"}, {\"id\" : 5, \"name\" : \"tag5\"}] |\n| 433bf6b7-3c9c-4035-b61c-57dc77a15862 | User2 | [2, 3] | [{\"id\" : 2, \"name\" : \"tag2\"}, {\"id\" : 3, \"name\" : \"tag3\"}] |\n+--------------------------------------+-------+--------+------------------------------------------------------------+\n```\n\nInsert new contact with 3 tags:\n\n```sql\nINSERT INTO main.contacts\n(\n    name,\n    tags\n)\nVALUES (\n    'User1',\n    main.get_and_maybe_insert_contact_tags(ARRAY['tag4', 'tag5', 'tag6'])\n);\n```\n\nUpdate contact tags:\n\n```sql\nUPDATE main.contacts\nSET\n    tags = (main.get_and_maybe_insert_contact_tags(ARRAY['tag6', 'tag7']))\nWHERE\n    name = 'User5';\n```\n\nFetch all tags and the number of contacts associated with each:\n\n```sql\nSELECT\n    contact_tags.name,\n    COUNT(contacts.id) AS contact_count\nFROM\n    main.contact_tags\nLEFT JOIN\n    main.contacts\nON\n    contact_tags.id = ANY(contacts.tags)\nGROUP BY contact_tags.id;\n+------+---------------+\n| name | contact_count |\n|------+---------------|\n| tag1 | 1             |\n| tag2 | 2             |\n| tag3 | 1             |\n| tag4 | 1             |\n| tag5 | 1             |\n+------+---------------+\n```\n\nList all contact associated with id `2`:\n\n```sql\npostgres=# SELECT * FROM main.contacts_with_tag_names WHERE 2 = ANY(tags);\n+--------------------------------------+-------+--------+------------------+\n| id                                   | name  | tags   | tag_names        |\n|--------------------------------------+-------+--------+------------------|\n| 92696bf1-7285-4ec0-bf90-39653d53a2d0 | User2 | [2, 3] | ['tag2', 'tag3'] |\n| a155c3b6-15b4-4f93-8ca8-9fc0634069af | User1 | [1, 2] | ['tag1', 'tag2'] |\n+--------------------------------------+-------+--------+------------------+\nSELECT 2\nTime: 0.011s\n```\n\nList all contact associated with `tag2`:\n\n```\npostgres# SELECT * FROM main.contacts_with_tag_names WHERE (SELECT id FROM main.con\n tact_tags WHERE name='tag2') = ANY(tags);\n+--------------------------------------+-------+--------+------------------+\n| id                                   | name  | tags   | tag_names        |\n|--------------------------------------+-------+--------+------------------|\n| 92696bf1-7285-4ec0-bf90-39653d53a2d0 | User2 | [2, 3] | ['tag2', 'tag3'] |\n| a155c3b6-15b4-4f93-8ca8-9fc0634069af | User1 | [1, 2] | ['tag1', 'tag2'] |\n+--------------------------------------+-------+--------+------------------+\nSELECT 2\nTime: 0.007s\n```\n\nAnother query based on [`\u0026\u0026` operator](https://www.postgresql.org/docs/16/functions-array.html):\n\n```\npostgres# (\n    SELECT *\n    FROM main.contacts_with_tag_names\n    WHERE (\n        tags \u0026\u0026 (\n            SELECT ARRAY_AGG(id) FROM main.contact_tags WHERE name = ANY(ARRAY['tag2'])\n        )\n    )\n);\n+--------------------------------------+-------+--------+------------------+\n| id                                   | name  | tags   | tag_names        |\n|--------------------------------------+-------+--------+------------------|\n| a155c3b6-15b4-4f93-8ca8-9fc0634069af | User1 | [1, 2] | ['tag1', 'tag2'] |\n| 92696bf1-7285-4ec0-bf90-39653d53a2d0 | User2 | [2, 3] | ['tag2', 'tag3'] |\n+--------------------------------------+-------+--------+------------------+\n```\n\nQuery to filter contacts which are linked to `tag1` or `tag5`:\n\n```\npostgres# (\n    SELECT *\n    FROM main.contacts_with_tag_names\n    WHERE (\n        tags \u0026\u0026 (\n            SELECT ARRAY_AGG(id) FROM main.contact_tags WHERE name = ANY(ARRAY['tag1', 'tag5'])\n        )\n    )\n);\n+--------------------------------------+-------+--------+------------------+\n| id                                   | name  | tags   | tag_names        |\n|--------------------------------------+-------+--------+------------------|\n| a155c3b6-15b4-4f93-8ca8-9fc0634069af | User1 | [1, 2] | ['tag1', 'tag2'] |\n| 2a64014e-8e60-4959-b333-f385b61124b8 | User3 | [4, 5] | ['tag4', 'tag5'] |\n+--------------------------------------+-------+--------+------------------+\nSELECT 2\nTime: 0.007s\n```\n\nQuery to filter contacts which are linked to `tag2` and `tag3`, based on [`\u003c@`](https://www.postgresql.org/docs/16/functions-array.html) operator:\n\n```\npostgres# (\n    SELECT *\n    FROM main.contacts_with_tag_names\n    WHERE (\n        tags \u003c@ (\n            SELECT ARRAY_AGG(id) FROM main.contact_tags WHERE name = ANY(ARRAY['tag2', 'tag3'])\n        )\n    )\n);\n+--------------------------------------+-------+--------+------------------+\n| id                                   | name  | tags   | tag_names        |\n|--------------------------------------+-------+--------+------------------|\n| 92696bf1-7285-4ec0-bf90-39653d53a2d0 | User2 | [2, 3] | ['tag2', 'tag3'] |\n+--------------------------------------+-------+--------+------------------+\nSELECT 1\nTime: 0.007s\n```\n\nQuery to filter contacts which are linked to (`tag2` and `tag3`) or `tag1`:\n\n```\npostgres# (\n    SELECT *\n    FROM main.contacts_with_tag_names\n    WHERE (\n        (\n            tags \u003c@ (\n                SELECT ARRAY_AGG(id) FROM main.contact_tags WHERE name = ANY(ARRAY['tag2', 'tag3'])\n            )\n        ) OR (\n            tags \u0026\u0026 (\n                SELECT ARRAY_AGG(id) FROM main.contact_tags WHERE name = ANY(ARRAY['tag1'])\n            )\n        )\n    )\n);\n```\n\n\nIf you feel like it, you can use [pgcli](https://github.com/dbcli/pgcli) to experiment:\n\n```sh\n$ pip install -U pgcli\n```\n\n```sh\n$ ./scripts/pgcli.sh\nServer: PostgreSQL 15.2 (Debian 15.2-1.pgdg110+1)\nVersion: 3.5.0\nHome: http://pgcli.com\npostgres@127:postgres\u003e\n```\n\nExecute pgTAP tests:\n\n```\n$ ./scripts/tests.sh\n/sqls/tests/test1.sql ..\n1..10\nok 1 - Table main.contacts should exist\nok 2 - Table main.contact_tags should exist\nok 3\nok 4\nok 5\nok 6\nok 7\nok 8\nok 9\nok 10\nok\nAll tests successful.\nFiles=1, Tests=10,  0 wallclock secs ( 0.01 usr  0.00 sys +  0.01 cusr  0.00 csys =  0.02 CPU)\nResult: PASS\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstephane-klein%2Fpostgres-tags-model-poc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstephane-klein%2Fpostgres-tags-model-poc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstephane-klein%2Fpostgres-tags-model-poc/lists"}