{"id":23723097,"url":"https://github.com/mull/pppt","last_synced_at":"2026-04-26T16:32:37.219Z","repository":{"id":142396501,"uuid":"153122011","full_name":"mull/pppt","owner":"mull","description":"Postgres Please Perform This: Small monadic service objects for Sequel models","archived":false,"fork":false,"pushed_at":"2018-11-27T09:26:56.000Z","size":36,"stargazers_count":1,"open_issues_count":2,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-05-30T18:21:56.692Z","etag":null,"topics":["postgres","ruby","sequel"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mull.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","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,"zenodo":null}},"created_at":"2018-10-15T13:53:37.000Z","updated_at":"2020-03-04T18:23:45.000Z","dependencies_parsed_at":null,"dependency_job_id":"7a02a25a-3f04-4448-ab4c-7d85d8419295","html_url":"https://github.com/mull/pppt","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/mull/pppt","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mull%2Fpppt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mull%2Fpppt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mull%2Fpppt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mull%2Fpppt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mull","download_url":"https://codeload.github.com/mull/pppt/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mull%2Fpppt/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32305035,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-26T09:34:17.070Z","status":"ssl_error","status_checked_at":"2026-04-26T09:34:00.993Z","response_time":129,"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":["postgres","ruby","sequel"],"created_at":"2024-12-30T23:59:05.762Z","updated_at":"2026-04-26T16:32:37.212Z","avatar_url":"https://github.com/mull.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Pretty Please Perform This\nOpinionated service object generator for Sequel models.\n\nThe namespaces of PPPT are divided into the concept of complexity, currently only Single exists, and plurality.\n\n# Simple\nThere's no fuzz here. It handles composite primary keys easily.\n\nThe library will validate that no keys that don't exist on the model are attempted to be written to. If so it raises `PPPT::InvalidKeyError` with a message. Similarly if one tries to update the primary key of a row the same exception is raised.\n\n## Singular\n```ruby\nclass SomeModel \u003c Sequel::Model\n  # id SERIAL PRIMARY KEY\n  # name VARCHAR\n  # count INTEGER NOT NULL DEFAULT 0\nend\n```\n\n### Create\n```ruby\nclass SimpleSingularInsert \u003c PPPT::Simple::Single::Create(SomeModel); end\n\nSimpleSingularInsert.new.call(name: 'foo') # =\u003e Success(SomeModel#\u003cid: 1, name: 'foo')\n```\n\n### Update\nUpdate is very simple: given an instance and some params, update it.\n\n```ruby\nclass SimpleSingularUpdate \u003c PPPT::Simple::Single::Update(SomeModel); end\nSimpleSingularUpdate.new.call(SomeModel.first, name: 'bar') # =\u003e Success(SomeModel#\u003cid: 1, name: 'bar'\u003e)\n```\n\nHowever, update also guards against updating the primary key and raises an error when you try:\n\n```ruby\nSimpleSingularUpdate.new.call(SomeModel.first, id: 2) # =\u003e raises PPPT::InvalidKeyError\u003c\"The primary key (id) cannot be updated on SimpleModel\"\u003e\n```\n\nIn case the params match the current values and no update is performed the same instance of the model is returned:\n```ruby\nSimpleSingularUpdate.new.call(SomeModel.first, name: 'foo') # =\u003e Success(SomeModel#\u003cid: 1, name: 'foo')\n```\n\n### Delete\nDelete resolves to nil upon success, rather than returning a deleted model.\n\n```ruby\nclass SimpleSingularDelete \u003c PPPT::Simple::Single::Delete(SomeModel); end\nSimpleSingularDelete.new.call(SomeModel.first) # =\u003e Success(nil)\n```\n\n## Plural\n### Create\nTakes an array of hashes to insert. This results in only one call to Postgres (multi_insert), but will return instances of the model.\n\n```ruby\nclass SimplePluralCreate \u003c PPPT::Simple::Plural::Create(SomeModel); end\nSimplePluralCreate.new.call([{name: 'foo'}, {name: 'bar'}])\n# =\u003e Success([SimpleModel\u003cid: 1, name: 'foo'\u003e, SimpleModel\u003cid: 2, name: 'bar'\u003e])\n```\n\nCreation is a tiny bit smart though. If you refer back to the definition of SomeModel you'll see that the column `count` is non-nullable and has a default value. PPPT will look at the hashes given to it and ensure that all inserts have the equal amount of columns. It will first attempt to find the default values of the column from the model:\n\n```ruby\nSimplePluralCreate.new.call({name: 'foo', count: 1}, {name: 'bar'})\n# =\u003e INSERT INTO some_models (name, count) VALUES ('foo', 1), ('bar', 0)\n```\n\nIf the column has no default value it will attempt to use nil:\n\n```ruby\nSimplePluralCreate.new.call({count: 1}, {name: 'bar'})\n# =\u003e INSERT INTO some_models (name, count) VALUES (NULL, 1), ('bar', 0)\n```\n\n### Update\nUpdate takes array pairs of `[model, params]`. Like singular update it will prevent you from updating the primary key of a row.\n\n```ruby\nclass SimplePluralUpdate \u003c PPPT::Simple::Plural::Update(SomeModel); end\nSimplePluralUpdate.new.call([\n  [instance_a, name: 'foofoo'],\n  [instance_b, name: 'barbar'],\n])\n# =\u003e Success([SimpleModel\u003cid: 1, name: 'foofoo'\u003e, SimpleModel\u003cid: 2, name: 'barbar'])\n```\n\nAs of now plural update produces one SQL statement per model it updates.\n\n\n### Delete\nDeletion takes an array of model instances and deletes them in one statement.\n\n```ruby\nclass SimplePluralDelete \u003c PPPT::Simple::Plural::Delete(SomeModel); end\nSimplePluralDelete.new.call([instance_a, instance_b]) # =\u003e Success(2)\n```\n\n\n### Upsert\nUpsert makes use of Postgres' native [ON CONFLICT insertion](https://www.postgresql.org/docs/10/static/sql-insert.html). As of writing this (0.2.0) it only allows specifying a constraint name. By default it will do nothing (same behaviour as Sequel). For any keys to be updated they must be provided. All keys are defaulted to `EXCLUDED.column_name`. At some later stage this may change.\n\nUnlike other services this one makes no use of models and takes no models as input. It does however make use of the model by:\n\n1. Validating that the constraint given exists\n2. Validating that the keys specified exist as columns\n\nThe successful value of this service is:\n\n1. New rows inserted are returned\n2. Untouched rows, or existing rows, are left out\n\nMeaning **this service only returns the new rows that were created**.\n\n\nBy default we _DO NOTHING_:\n\n```ruby\nclass UpsertService \u003c PPPT::Simple::Plural::Upsert(ModelWithConstraint)\n  constraint :unique_constraint_on_column_a\nend\nUpsertService.new.call([{name: 'foo', a: 1}]) # =\u003e Success([])\n\n# INSERT INTO \"model_with_constraint\" (\"name\", \"a\") VALUES ('foo', 1) ON CONFLICT ON CONSTRAINT \"unique_constraint_on_column_a\" DO NOTHING RETURNING *\n```\n\nAdditionally we can explicitly say that the service is doing nothing:\n\n```ruby\nclass UpsertService \u003c PPPT::Simple::Plural::Upsert(ModelWithConstraint)\n  constraint :unique_constraint_on_column_a\n  do_nothing\nend\nUpsertService.new.call([{name: 'foo', a: 1}]) # =\u003e Success([])\n\n# INSERT INTO \"model_with_constraint\" (\"name\", \"a\") VALUES ('foo', 1) ON CONFLICT ON CONSTRAINT \"unique_constraint_on_column_a\" DO NOTHING RETURNING *\n```\n\nIf we specify the keys we want to update, we'll get that effect:\n\n```ruby\nclass UpsertService \u003c PPPT::Simple::Plural::Upsert(ModelWithConstraint)\n  constraint :unique_constraint_on_column_a\n  update :name\nend\nUpsertService.new.call([{name: 'foo', a: 1}]) # =\u003e Success([])\n\n# INSERT INTO \"model_with_constraint\" (\"name\", \"a\") VALUES ('foo', 1) ON CONFLICT ON CONSTRAINT \"unique_constraint_on_column_a\" DO UPDATE SET \"name\" = \"excluded\".\"name\" RETURNING *\n```\n\n# One to Many\nHandle creation of parents and children.\n\n## Plural\n\n### Create\nGiven services as definition time to create children for a model's association we can easily create multiple parents and children without writing glue code to fill in the foreign keys. Sequel models lets us know how to fill in the associations, yet the inserts are done in batch.\n\n\n```ruby\nclass CreateChapters \u003c PPPT::Simple::Plural::Create(Chapter); end\n\nclass CreateBooksAndChapters \u003c PPPT::OneToMany::Plural::Create(Book)\n  create_chapters CreateChapters.new\nend\n\nCreateBooksAndChapters.new.call([\n  {\n    title: 'Eloquent Ruby',\n    chapters: [\n      { title: 'Write code that looks like Ruby' },\n      { title: 'Choose the Right Control Structure' }\n    ]\n  },\n  {\n    title: 'Ruby under a microscope',\n    chapters: [\n      { title: 'Tokenization and Parsing' },\n      { title: 'Compilation' }\n    ]\n  }\n])\n\n# INSERT INTO books (title) VALUES ('Eloquent Ruby'), ('Ruby under a microscope') RETURNING *\n# INSERT INTO chapters (book_id, title) VALUES (1, 'Write code that looks like Ruby'), (1, 'Choose the Right Control Structure'), (2, 'Tokenization and Parsing'), (2, 'Compilation')\n\n# =\u003e # =\u003e Success([Book\u003cid: 1, title: 'Eloquent Ruby'\u003e, Book\u003cid: 2, title: 'Ruby under a microscope'\u003e])\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmull%2Fpppt","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmull%2Fpppt","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmull%2Fpppt/lists"}