Author: @mkruskal-google
Approved: 2023-08-25
One of the things Life of an Edition lays out is a very loose scheme for naming editions. It defines an ordering rule and “.” delimiter, but otherwise imposes no constraints on the edition name. By convention, we decided to use the year followed by an optional revision number.
One of the decisions in Editions: Life of a FeatureSet was that feature resolution would, at least partially, need to be duplicated across every supported language.
As discussed in Life of a FeatureSet, the minimal operations we need to duplicate are edition comparison and proto merging. While edition comparison isn't very complicated today, it has a lot of edge cases we may miss due to the loose constraints on edition names. It would also be really nice if we could reduce it down to a lexicographical string comparison, which can be easily done in any language.
There‘s also a very real Hyrum’s law risk when we permit an infinite number of edition names that we never expect to exist in practice. For example, 2023.a
is currently valid, and its relative ordering to 2023.10
(for example) is not obvious. Notably, our initial editions tests used an edition very-cool
, which doesn't seem like something we need to support. Ideally, our edition names would be as simple as possible, with enforceable and documented constraints.
There has been a lot of confusion when looking at the interaction between editions and releases. Specifically, the fact that editions look like calver numbers has led to us calling revisions “patch editions”, which suggests that they're bug fixes to an earlier one. This was not the original intention though, which to summarize was:
Editions are strictly time-ordered. Revisions were simply a mechanism to release more than one edition per calendar year, but new editions can't be inserted into earlier slots.
New editions can be added at any time. As long as they're ordered after the pre-existing ones, this is a non-breaking change and can be done in a patch release.
New features can be added at any time without changing the edition. Since they're a non-breaking change by definition, this can be done in a patch release.
Features can only be deleted in a breaking release. The editions model doesn't support the deletion of features, which is always a breaking change. This will only be done in a major version bump of protobuf.
Feature defaults can only be changed in a new edition. Once the default is chosen for a feature, we can only change it by releasing a new edition with an updated default. This is a non-breaking change though, and can be done in patch releases.
We want the following properties out of editions:
The simplest solution here is to just make an Edition
enum for specifying the edition. We will continue to use strings in proto files, but the parser will quickly convert them and all code from then on will treat them as enums. This way, we will have a centralized cross-language list of all valid editions:
enum Edition { EDITION_UNKNOWN = 0; EDITION_2023 = 1; EDITION_2024 = 2; // ... }
We will document that these are intended to be comparable numerically for finding the time ordering of editions. Proto files will specify the edition in exactly the same way as in the original decision:
edition = "2023";
Ideally, this would be an open enum to avoid ever having the edition thrown into the unknown field set. However, since it needs to exist in descriptor.proto
, we won't be able to make it open until the end of our edition zero migration. Until then, we can take advantage of the fact that syntax
gets set to editions
by the parser when an edition is found. In this case, an unset edition should be treated as an error. Once we migrate to an open enum we can replace this with a simpler validity check.
Additionally, we‘ve decided to punt on the question of revisions and just not allow them for now. If we ever need more than that it means we’ve made a huge mistake, and we can bikeshed on naming at that time (e.g. EDITION_2023_OOPS
). We will plan to have exactly one edition every year.
Edition comparison becomes even simpler, as it's just an integer comparison
Automatic rejection of unknown editions.
Doesn't look like calver, avoiding that confusion
Lack of revisions simplifies our documentation and makes editions easier to understand/maintain
Might prove to be a challenge when we go to migrate descriptor.proto
to editions
Might be a bit tricky to implement in the parser (but Prototiller does this just fine)
Instead of using a string, editions could be represented as an enum. This would give us a lot of enforcement for free and be maximally constraining. For example:
enum Edition { E2023 = 1; E2023A = 2; E2024 = 3; }
Edition comparison becomes even simpler, as it's just an integer comparison
Arbitrary number of revisions within a year
Automatic rejection of unknown editions (with closed enums).
Doesn't look like calver, avoiding that confusion
Edition specification in every proto file becomes arguably less readable. edition = "2023.1"
is easier to read than edition = E2023A
.
Might require parser changes to get descriptor.proto
onto editions
This is a pretty big change and will require documentation/comms updates
Plugin owners can't pre-release upcoming features
Edition must be strictly time-ordered. We can‘t go back and add a revision to an older revision, but the original plan didn’t really allow for this anyway.
Edition ordering is not directly linked to the name at all. We‘d have to write a reflection test that enforces that they’re strictly increasing.
The simplest solution that's consistent with the original plan would be to limit ourselves to no more than 9 intermediate editions per year. This means editions would either be a simple year (e.g. 2023
), or have a single digit revision number (e.g. 2024.3
). The revision number would be constrained to (0,9]
and the year would have to be an integer >=2023
.
With these constraints in place, edition ordering becomes a simple lexicographical string ordering.
2023.0
isn't allowed)This is similar to the recommended solution, but instead of allowing year editions we would always start with a .0
release. This means that Edition Zero would become 2023.0
. This has the same pros/cons as the solution above, with the addition of:
2023.1
if they’re used to 2023.0
.2023
. We’d have to go update those and communicate the change.Instead of using a string, editions could be represented as a message. This would allow us to model the constraints better. For example:
message Edition { uint32 year = 1; uint32 revision = 2; }