{"id":13684456,"url":"https://github.com/icings/partitionable","last_synced_at":"2025-04-30T21:30:29.449Z","repository":{"id":41599286,"uuid":"370419309","full_name":"icings/partitionable","owner":"icings","description":"Partitionable associations for the CakePHP ORM, allowing for basic limiting per group.","archived":false,"fork":false,"pushed_at":"2024-07-03T10:58:15.000Z","size":90,"stargazers_count":16,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"2.x","last_synced_at":"2025-03-27T12:50:25.288Z","etag":null,"topics":["associations","cakephp","database","greatest-n-per-group","limit","orm"],"latest_commit_sha":null,"homepage":"","language":"PHP","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/icings.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","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":"2021-05-24T16:36:13.000Z","updated_at":"2024-07-03T10:57:28.000Z","dependencies_parsed_at":"2023-12-19T05:24:46.425Z","dependency_job_id":"c8510df9-99cc-468b-b00f-a6b9dd67cd03","html_url":"https://github.com/icings/partitionable","commit_stats":{"total_commits":32,"total_committers":1,"mean_commits":32.0,"dds":0.0,"last_synced_commit":"7e920f2b7842573d45ec6f80d87000240cccc600"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icings%2Fpartitionable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icings%2Fpartitionable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icings%2Fpartitionable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icings%2Fpartitionable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/icings","download_url":"https://codeload.github.com/icings/partitionable/tar.gz/refs/heads/2.x","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251284307,"owners_count":21564611,"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":["associations","cakephp","database","greatest-n-per-group","limit","orm"],"created_at":"2024-08-02T14:00:33.774Z","updated_at":"2025-04-30T21:30:29.105Z","avatar_url":"https://github.com/icings.png","language":"PHP","funding_links":[],"categories":["ORM / Database / Datamapping","Plugins"],"sub_categories":["ORM / Database / Datamapping"],"readme":"# Partitionable\n\n[![Build Status][ico-build]][link-build]\n[![Coverage Status][ico-coverage]][link-coverage]\n[![Latest Version][ico-version]][link-version]\n[![Software License][ico-license]][link-license]\n\n[ico-build]: https://img.shields.io/github/actions/workflow/status/icings/partitionable/ci.yml?branch=2.x\u0026style=flat-square\n[ico-coverage]: https://img.shields.io/codecov/c/github/icings/partitionable/2.x.svg?style=flat-square\n[ico-version]: https://img.shields.io/packagist/v/icings/partitionable.svg?style=flat-square\u0026label=latest\n[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square\n\n[link-build]: https://github.com/icings/partitionable/actions/workflows/ci.yml?query=branch%3A2.x\n[link-coverage]: https://codecov.io/github/icings/partitionable/tree/2.x\n[link-version]: https://packagist.org/packages/icings/partitionable\n[link-license]: LICENSE.txt\n\nA set of partitionable associations for the CakePHP ORM, allowing for basic limiting per group.\n\n\n## Requirements\n\n* CakePHP ORM 5.0+ (use the [1.x branch](https://github.com/icings/partitionable/tree/1.x) of this plugin if you're\n  looking for CakePHP 4 compatibility).\n* A DBMS supported by CakePHP with window function support (MySQL 8, MariaDB 10.2, Postgres 9.4, SQL Sever 2017,\n  Sqlite 3.25).\n\n\n## Installation\n\nUse [Composer](https://getcomposer.org) to add the library to your project:\n\n```bash\ncomposer require icings/partitionable\n```\n\n\n## I don't get it, what is this good for exactly?\n\nWhat exactly are these associations good for, what is a \"_limit per group_\" you may ask.\n\nBasically the associations provided in this library allow applying limits for `hasMany` and `belongsToMany` type of\nassociations, so that it's possible to for example receive a maximum of _n_ number of comments per article for an\n`Articles hasMany Comments` association.\n\n\n## Usage\n\n**Make sure to first check the [Known Issues/Limitations](#known-issueslimitations) section!**\n\nThen add the `\\Icings\\Partitionable\\ORM\\AssociationsTrait` trait to your table class, use its `partitionableHasMany()`\nand `partitionableBelongsToMany()` methods to add `hasMany`, respectively `belongsToMany` associations, configure a\nlimit and a sort order, and you're done with the minimal setup and you can contain the partitionable associations just\nlike any other associations.\n\nNote that configuring a sort order is mandatory, as it is not possible to reliably partition the results without an\nexplicit sort order, omitting it will result in an error!\n\n### Has Many\n\n```php\n// ...\nuse Icings\\Partitionable\\ORM\\AssociationsTrait;\n\nclass ArticlesTable extends \\Cake\\ORM\\Table\n{\n    use AssociationsTrait;\n\n    public function initialize(array $config): void\n    {\n        // ...\n\n        $this\n            -\u003epartitionableHasMany('TopComments')\n            -\u003esetClassName('Comments')\n            -\u003esetLimit(3)\n            -\u003esetSort([\n                'TopComments.votes' =\u003e 'DESC',\n                'TopComments.id' =\u003e 'ASC',\n            ]);\n    }\n}\n```\n\n```php\n$articlesQuery = $this-\u003eArticles\n    -\u003efind()\n    -\u003econtain('TopComments');\n```\n\nThat would query the 3 highest voted comments for each article, eg the result would look something like:\n\n```php\n[\n    'title' =\u003e 'Some Article',\n    'top_comments' =\u003e [\n        [\n            'votes' =\u003e 10,\n            'body' =\u003e 'Some Comment',\n        ],\n        [\n            'votes' =\u003e 9,\n            'body' =\u003e 'Some Other Comment',\n        ],\n        [\n            'votes' =\u003e 8,\n            'body' =\u003e 'And Yet Another Comment',\n        ],\n    ],\n]\n```\n\n### Belongs To Many\n\n```php\n// ...\nuse Icings\\Partitionable\\ORM\\AssociationsTrait;\n\nclass StudentsTable extends \\Cake\\ORM\\Table\n{\n    use AssociationsTrait;\n\n    public function initialize(array $config): void\n    {\n        // ...\n\n        $this\n            -\u003epartitionableBelongsToMany('TopGraduatedCourses')\n            -\u003esetClassName('Courses')\n            -\u003esetThrough('CourseMemberships')\n            -\u003esetLimit(3)\n            -\u003esetSort([\n                'CourseMemberships.grade' =\u003e 'ASC',\n                'CourseMemberships.id' =\u003e 'ASC',\n            ])\n            -\u003esetConditions([\n                'CourseMemberships.grade IS NOT' =\u003e null,\n            ]);\n    }\n}\n```\n\n```php\n$studentsQuery = $this-\u003eStudents\n    -\u003efind()\n    -\u003econtain('TopGraduatedCourses');\n```\n\nThat would query the 3 highest graduated courses for each student, eg the result would look something like:\n\n```php\n[\n    'name' =\u003e 'Some Student',\n    'top_graduated_courses' =\u003e [\n        [\n            'name' =\u003e 'Some Course',\n            '_joinData' =\u003e [\n                'grade' =\u003e 1,\n            ],\n        ],\n        [\n            'body' =\u003e 'Some Other Course',\n            '_joinData' =\u003e [\n                'grade' =\u003e 2,\n            ],\n        ],\n        [\n            'body' =\u003e 'And Yet Another Course',\n            '_joinData' =\u003e [\n                'grade' =\u003e 3,\n            ],\n        ],\n    ],\n]\n```\n\n### Using options to configure the associations\n\nAdditionally to the chained method call syntax, options as known from the built-in associations are supported too,\nspecifically the following options are supported for both `partitionableHasMany()` as well as\n`partitionableBelongsToMany()`:\n\n* `limit` (`int|null`)\n* `singleResult` (`bool`)\n* `filterStrategy` (`string`)\n\n```php\n$this\n    -\u003epartitionableHasMany('TopComments', [\n        'className' =\u003e 'Comments',\n        'limit' =\u003e 1,\n        'singleResult' =\u003e false,\n        'filterStrategy' =\u003e \\Icings\\Partitionable\\ORM\\Association\\PartitionableHasMany::FILTER_IN_SUBQUERY_TABLE,\n        'sort' =\u003e [\n          'TopComments.votes' =\u003e 'DESC',\n          'TopComments.id' =\u003e 'ASC',\n        ],\n    ]);\n```\n\n### Changing settings on the fly\n\nThe limit and the sort order can be applied/changed on the fly in the containment's query builder:\n\n```php\n$articlesQuery = $this-\u003eArticles\n    -\u003efind()\n    -\u003econtain('TopComments', function (\\Cake\\ORM\\Query\\SelectQuery $query) {\n        return $query\n            -\u003elimit(10)\n            -\u003eorder([\n                'TopComments.votes' =\u003e 'DESC',\n                'TopComments.id' =\u003e 'ASC',\n            ]);\n    });\n```\n\nand via `Model.beforeFind`, where the partitionable fetcher query that needs to be modified, can be identified via the\noption `partitionableQueryType`, which would hold the value `fetcher`:\n\n```php\n$this-\u003eArticles-\u003eTopComments\n    -\u003egetEventManager()\n    -\u003eon('Model.beforeFind', function ($event, \\Cake\\ORM\\Query\\SelectQuery $query, \\ArrayObject $options) {\n        if (($options['partitionableQueryType'] ?? null) === 'fetcher') {\n            $query\n              -\u003elimit(10)\n              -\u003eorder([\n                  'TopComments.votes' =\u003e 'DESC',\n                  'TopComments.id' =\u003e 'ASC',\n              ]);\n        }\n        \n        return $query;\n    });\n```\n\n### Limiting to a single result\n\nWhen setting the limit to `1`, the associations will automatically switch to using singular property names (if no \nproperty name has been set yet), and non-nested results.\n\nFor example, limiting this association to `1`:\n\n```php\n$this\n    -\u003epartitionableHasMany('TopComments')\n    -\u003esetClassName('Comments')\n    -\u003esetLimit(1)\n    -\u003esetSort([\n        'TopComments.votes' =\u003e 'DESC',\n        'TopComments.id' =\u003e 'ASC',\n    ]);\n```\n\nwould return a result like this:\n\n```php\n[\n    'title' =\u003e 'Some Article',\n    'top_comment' =\u003e [\n        'votes' =\u003e 10,\n        'body' =\u003e 'Some Comment',\n    ],\n]\n```\n\nwhile a limit of greater or equal to `2`, would return a result like this:\n\n```php\n[\n    'title' =\u003e 'Some Article',\n    'top_comments' =\u003e [\n        [\n            'votes' =\u003e 10,\n            'body' =\u003e 'Some Comment',\n        ],\n        [\n            'votes' =\u003e 5,\n            'body' =\u003e 'Some Other Comment',\n        ],\n    ],\n]\n```\n\nThis behavior can be disabled using the association's `disableSingleResult()` method, and likewise _enabled_ using\n`enableSingleResult()`. Calling the latter will also cause the limit to be set to `1`. Furthermore, setting the limit\nto greater or equal to `2`, will automatically disable the single result mode.\n\nWith the single result mode disabled:\n\n```php\n$this\n    -\u003epartitionableHasMany('TopComments')\n    -\u003esetClassName('Comments')\n    -\u003esetLimit(1)\n    -\u003edisableSingleResult()\n    -\u003esetSort([\n        'TopComments.votes' =\u003e 'DESC',\n        'TopComments.id' =\u003e 'ASC',\n    ]);\n```\n\na limit of `1` would return a result like this:\n\n```php\n[\n    'title' =\u003e 'Some Article',\n    'top_comments' =\u003e [\n        [\n            'votes' =\u003e 10,\n            'body' =\u003e 'Some Comment',\n        ],\n    ],\n]\n```\n\n### Filter Strategies\n\nThe associations currently provide a few different filter strategies that affect how the query that obtains the\nassociated data is being filtered.\n\nNot all queries are equal, while one strategy may work fine for one query, it might cause problems for another.\n\nThe strategy can be set using the association's `setFilterStrategy()` method:\n\n```php\nuse Icings\\Partitionable\\ORM\\Association\\PartitionableHasMany;\n\n// ...\n\n$this\n    -\u003epartitionableHasMany('TopComments')\n    -\u003esetClassName('Comments')\n    -\u003esetFilterStrategy(PartitionableHasMany::FILTER_IN_SUBQUERY_TABLE)\n    -\u003esetLimit(3)\n    -\u003esetSort([\n        'TopComments.votes' =\u003e 'DESC',\n        'TopComments.id' =\u003e 'ASC',\n    ]);\n```\n\nPlease refer to the API docs for SQL examples of how the different strategies work:\n\n* [`\\Icings\\Partitionable\\ORM\\Association\\Loader\\PartitionableSelectLoader`](\n  src/ORM/Association/Loader/PartitionableSelectLoader.php)\n* [`\\Icings\\Partitionable\\ORM\\Association\\Loader\\PartitionableSelectWithPivotLoader`](\n  src/ORM/Association/Loader/PartitionableSelectWithPivotLoader.php)\n\nThe currently available strategies are:\n\n* `\\Icings\\Partitionable\\ORM\\Association\\PartitionableAssociationInterface::FILTER_IN_SUBQUERY_CTE`\n* `\\Icings\\Partitionable\\ORM\\Association\\PartitionableAssociationInterface::FILTER_IN_SUBQUERY_JOIN`\n* `\\Icings\\Partitionable\\ORM\\Association\\PartitionableAssociationInterface::FILTER_IN_SUBQUERY_TABLE` (default)\n* `\\Icings\\Partitionable\\ORM\\Association\\PartitionableAssociationInterface::FILTER_INNER_JOIN_CTE`\n* `\\Icings\\Partitionable\\ORM\\Association\\PartitionableAssociationInterface::FILTER_INNER_JOIN_SUBQUERY`\n\n\n## Known Issues/Limitations\n\n* These associations are **_not_** meant for save or delete operations, _only_ for read operations!\n\n* MySQL 5 is not supported as it doesn't support the required window functions used for row numbering. While it's\n  possible to emulate the required row numbering, these constructs are rather fragile and there's way too many\n  situations in which they will break, respectively silently produce wrong results.\n\n* MariaDB \u003e= 11.0 can crash with the `FILTER_IN_SUBQUERY_CTE`, and the default `FILTER_IN_SUBQUERY_TABLE` strategy when\n  using the translate behavior with `onlyTranslated` enabled (https://jira.mariadb.org/browse/MDEV-31793). It is\n  strongly advised to use a different filter strategy when you find yourself with that version and translate behavior\n  config constellation!\n\n* Older MariaDB versions, when running in `ONLY_FULL_GROUP_BY` mode, erroneously require a `GROUP BY` clause to be\n  present when using window functions like the one used for row numbering (https://jira.mariadb.org/browse/MDEV-17785).\n  If you cannot use a version where that bug was fixed, you either have to disable `ONLY_FULL_GROUP_BY`, or add grouping\n  to the association's query accordingly.\n\n* SQL Server does not support common table expressions in subqueries, hence the `FILTER_IN_SUBQUERY_CTE` strategy cannot\n  be used with it. In fact, it's also not possible to use custom common table expressions in the association's query\n  with any other strategy, as it would result in the expression to be used in a subquery too, or nested in another\n  common table expression, which also isn't supported.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ficings%2Fpartitionable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ficings%2Fpartitionable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ficings%2Fpartitionable/lists"}