{"id":18019290,"url":"https://github.com/andytill/aversion","last_synced_at":"2025-08-29T21:37:33.583Z","repository":{"id":55043579,"uuid":"61475959","full_name":"andytill/aversion","owner":"andytill","description":"erlang parse transform for versioned records (work in progress)","archived":false,"fork":false,"pushed_at":"2021-01-13T11:27:07.000Z","size":88,"stargazers_count":3,"open_issues_count":2,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-04T16:38:48.446Z","etag":null,"topics":["downgrade","parse-transform","upgrade"],"latest_commit_sha":null,"homepage":"","language":"Erlang","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andytill.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"COPYING","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-06-19T10:55:48.000Z","updated_at":"2020-12-30T18:17:28.000Z","dependencies_parsed_at":"2022-08-14T10:00:32.296Z","dependency_job_id":null,"html_url":"https://github.com/andytill/aversion","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/andytill/aversion","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andytill%2Faversion","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andytill%2Faversion/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andytill%2Faversion/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andytill%2Faversion/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andytill","download_url":"https://codeload.github.com/andytill/aversion/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andytill%2Faversion/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272766903,"owners_count":24989407,"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","status":"online","status_checked_at":"2025-08-29T02:00:10.610Z","response_time":87,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["downgrade","parse-transform","upgrade"],"created_at":"2024-10-30T05:09:43.046Z","updated_at":"2025-08-29T21:37:33.563Z","avatar_url":"https://github.com/andytill.png","language":"Erlang","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Aversion\n\n![Affero GPL v3 Licensed](https://www.gnu.org/graphics/agplv3-88x31.png)\n\n**Aversion is very much a work in progress. Not all of the features described are working, it is as much a spec as docs at the moment. If you would like to contribute then please write up a use case as an issue, or a test module with a failing test. See the tests in the test directory for examples.**\n\nAversion is a parse transform for handling multiple versions of an erlang record that are **backwards** and **forwards** compatible, while still supporting the erlang record syntax.\n\nThe same record is defined multiple times, the version field must be the first field. Other field names may not be duplicated. Each record version includes all of the fields of the previous versions.\n\n```erlang\n%% the original definition for the project record, defined for an\n%% old version of our application\n-record(project, {\n    version = 1,\n    name :: binary()\n}).\n\n%% a new 'migration', project records with a value of 2 in the version will will have a\n%% commits field, the tuple will be three elements.\n-record(project, {\n    version = 2,\n    commits = 0 :: integer()\n}).\n```\n\nThe aversion parse transform will use these record definitions to generate functions that can manipulate different versions of the records. The parse transform inserts calls to the generated functions wherever the record is used. This is similar to having a module API for a record, but is transparent and supports the normal erlang record syntax.\n\nSpecific versions of records can be created.\n\n```erlang\n%% version 1\n{project, 1, \u003c\u003c\"erlyberly\"\u003e\u003e} = #project{ version = 1, name = \u003c\u003c\"erlyberly\"\u003e\u003e }.\n\n%% version 2, this has the commits field at element 4 which is defaulted to 0\n{project, 2, \u003c\u003c\"erlyberly\"\u003e\u003e, 0} = #project{ version = 2, name = \u003c\u003c\"erlyberly\"\u003e\u003e }.\n```\n\nIf the `version` field is not specified, then the latest version of the record will be created. \n\n```erlang\n{project, 2, \u003c\u003c\"erlyberly\"\u003e\u003e, 0} = #project{ name = \u003c\u003c\"erlyberly\"\u003e\u003e }.\n```\n\n### Scenarios\n\n##### Record version is higher than what is known\n\nIf the fields that are accessed are in the record version that we know, then the just access those fields and leave the other fields alone.  This is the likely situation, since we won't be using fields we're not aware of.\n\nIf we wish to modify those fields, just set elements and keep the fields we are not aware of.  The version value is not changed.\n\n##### Record version does not have a field in a newer version\n\nIf the field is being unpacked and the field has a default value, then return the default value. If it does not have a default value it must throw an exception. It is highly recommended to set a default value for every new field.\n\nIf the record does not have a field and we want to write the field version to it, then the record must be upgraded to the version that field was defined in.\n\n##### The node must send records of a specific version because the cluster has mixed versions\n\nSpecifying the `version` field in a record will create a record of that version.\n\nThe application needs to know what is the highest supported version in the cluster. Applications built with riak_core should know the version number that can be created using capabilities.\n\nIf a record needs to be downgraded in a way that results in data loss then the request that initiated the work must be rejected as not supported by the cluster.\n\n### Worked Example\n\n##### Currently in riak\\_core, record `riak_vnode_req_vX`\n\n`riak_core` defines two different versions of record for [fold requests](https://github.com/basho/riak_core/blob/develop/include/riak_core_vnode.hrl#L24).\n\n```erlang\n-record(riak_core_fold_req_v1, {\n          foldfun :: fun(),\n          acc0 :: term()}).\n-record(riak_core_fold_req_v2, {\n          foldfun :: fun(),\n          acc0 :: term(),\n          forwardable :: boolean(),\n          opts = [] :: list()}).\n\n\n-define(VNODE_REQ, #riak_vnode_req_v1).\n-define(COVERAGE_REQ, #riak_coverage_req_v1).\n-define(FOLD_REQ, #riak_core_fold_req_v2).\n```\n\nAnd an upgrade function in [riak_core_util.erl](https://github.com/basho/riak_core/blob/develop/src/riak_core_util.erl#L599).\n\n```erlang\n%% @doc Convert a #riak_core_fold_req_v? record to the cluster's maximum\n%%      supported record version.\n\nmake_fold_req(#riak_core_fold_req_v1{foldfun=FoldFun, acc0=Acc0}) -\u003e\n    make_fold_req(FoldFun, Acc0, false, []);\nmake_fold_req(?FOLD_REQ{foldfun=FoldFun, acc0=Acc0,\n                       forwardable=Forwardable, opts=Opts}) -\u003e\n    make_fold_req(FoldFun, Acc0, Forwardable, Opts).\n\nmake_fold_req(FoldFun, Acc0) -\u003e\n    make_fold_req(FoldFun, Acc0, false, []).\n\nmake_fold_req(FoldFun, Acc0, Forwardable, Opts) -\u003e\n    make_fold_reqv(riak_core_capability:get({riak_core, fold_req_version}, v1),\n                   FoldFun, Acc0, Forwardable, Opts).\n\n%% @doc Force a #riak_core_fold_req_v? record to the latest version,\n%%      regardless of cluster support\n\nmake_newest_fold_req(#riak_core_fold_req_v1{foldfun=FoldFun, acc0=Acc0}) -\u003e\n    make_fold_reqv(v2, FoldFun, Acc0, false, []);\nmake_newest_fold_req(?FOLD_REQ{} = F) -\u003e\n    F.\n\n%% @private\nmake_fold_reqv(v1, FoldFun, Acc0, _Forwardable, _Opts)\n  when is_function(FoldFun, 3) -\u003e\n    #riak_core_fold_req_v1{foldfun=FoldFun, acc0=Acc0};\nmake_fold_reqv(v2, FoldFun, Acc0, Forwardable, Opts)\n  when is_function(FoldFun, 3)\n       andalso (Forwardable == true orelse Forwardable == false)\n       andalso is_list(Opts) -\u003e\n    ?FOLD_REQ{foldfun=FoldFun, acc0=Acc0,\n              forwardable=Forwardable, opts=Opts}.\n```\n\nThere is no downgrade function. This is not required like it is in Riak TS because the records are not persisted in the metadata like the DDL is. Capabilities are used to determine which version of the record should be created. Defaults are used to upgrade the record but are \n\nWhen the vnode receives a `v1` version of the fold record it upgrades. The upgrade happens at the edge of the system so that the record is always at the current \n\n```erlang\nhandle_command(#riak_core_fold_req_v1{} = ReqV1,\n               Sender, State) -\u003e\n    %% Use make_fold_req() to upgrade to the most recent ?FOLD_REQ\n    handle_command(riak_core_util:make_newest_fold_req(ReqV1), Sender, State);\nhandle_command(?FOLD_REQ{foldfun=FoldFun, acc0=Acc0,\n                         forwardable=_Forwardable, opts=Opts}, Sender, State) -\u003e\n    %% The riak_core layer takes care of forwarding/not forwarding, so\n    %% we ignore forwardable here.\n    %%\n    %% The function in riak_core used for object folding expects the\n    %% bucket and key pair to be passed as the first parameter, but in\n    %% riak_kv the bucket and key have been separated. This function\n    %% wrapper is to address this mismatch.\n    FoldWrapper = fun(Bucket, Key, Value, Acc) -\u003e\n                          FoldFun({Bucket, Key}, Value, Acc)\n                  end,\n    do_fold(FoldWrapper, Acc0, Sender, Opts, State);\n```\n\n##### Using Aversion\n\nThe records need to be defined with the same name, with version fields. Macros for record names is unnecessary because the record name is always the same.\n\n```erlang\n-record(riak_core_fold_req, {\n          version = 1,\n          foldfun :: fun(),\n          acc0 :: term()}).\n-record(riak_core_fold_req, {\n          version = 2,\n          forwardable = false :: boolean(),\n          opts = [] :: list()}).\n```\n\nThe `forwardable` has been given the default that it would be created with, with in the upgrade function.\n\nThe vnode command handling will look like this.\n\n```erlang\nhandle_command(#riak_core_fold_req{foldfun=FoldFun, acc0=Acc0,\n                                   forwardable=_Forwardable, \n                                   opts=Opts}, Sender, State) -\u003e\n    %% The riak_core layer takes care of forwarding/not forwarding, so\n    %% we ignore forwardable here.\n    %%\n    %% The function in riak_core used for object folding expects the\n    %% bucket and key pair to be passed as the first parameter, but in\n    %% riak_kv the bucket and key have been separated. This function\n    %% wrapper is to address this mismatch.\n    FoldWrapper = fun(Bucket, Key, Value, Acc) -\u003e\n                          FoldFun({Bucket, Key}, Value, Acc)\n                  end,\n    do_fold(FoldWrapper, Acc0, Sender, Opts, State);\n```\n\nIf the `riak_core_fold_req` version fields has a value of 1 then forwardable and opts will be given the default values specified in the record. If it is version 2 then the the record values will be used.\n\nCreating the record can be done by using the latest version, and have nodes that don't understand this version use the fields that their version allows. If no version is specified, the latest version is assumed.\n\nOr, use capabilities to create the version that the cluster fully supports. This is similar to the `make_fold_req/4` function above.\n\n```erlang\nVersionNumber = riak_core_capability:get({riak_core, fold_req_version}, 1),\n#riak_core_fold_req{ version=VersionNumber, foldfun=FoldFun, acc0=Acc0 }.\n```\n\n**NOTE:** how to set fields on a record version that doesn't support those fields?  The example in riak_core just drops them, need to think what is best.\n\n### Utility Functions\n\n##### `is_safely_downgradable/1`\n\n```erlang\nis_safely_downgradable(NewVersion::integer(), Record::tuple()) -\u003e boolean().\n```\n\nIf the fields in the record that were added are equal to the default values then the record is safe to downgrade because no data is lost.\n\n##### `upgrade/2`\n\nAdd default values for fields that were added from the given record version to target version.\n\n##### `downgrade/2`\n\nThrows an error if the record is not safely downgradable.\n\n##### `force_downgrade/2`\n\nYOLO downgrade, chop the record fields that do not exist in the requested version.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandytill%2Faversion","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandytill%2Faversion","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandytill%2Faversion/lists"}