Skip to content

feat: Add simple enumerations to type system#6104

Open
kgutwin wants to merge 16 commits into
PRQL:mainfrom
bioteam:kg/4501/named-union-func-param
Open

feat: Add simple enumerations to type system#6104
kgutwin wants to merge 16 commits into
PRQL:mainfrom
bioteam:kg/4501/named-union-func-param

Conversation

@kgutwin

@kgutwin kgutwin commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Resolves #4501.

This PR adds a new Enum type which takes a "literal tuple" (a tuple with all fields named and having literal values). This type can be used to represent simple explicit enumerations of a database column or a function parameter.

In addition, special-case handling of name resolution was added to function parameters having been assigned an Enum type. In this case, the Enum module namespace is preferentially searched prior to any other namespaces. This allows for functions such as join or tuple_uniq to have their option parameters (such as side) be associated with an Enum and be fully syntax backwards compatible.

Unfortunately, the discussion in #4501 that centered around the use of Named Unions to implement enumerations was not able to be followed, since named unions were removed in PR #5111. The design of the "simple enumeration" was carefully chosen such that when the type system is enhanced in the future to once again support sum types and named unions, the existing syntax can be maintained for backwards compatibility reasons yet have its implementation updated to align with sum types.

@prql-bot prql-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice feature — the design of storing enum members as a module (with NS_SELF for the type and one Decl per alias) so bare option names resolve is clean, and it keeps join side:left etc. backwards-compatible. I built it locally and confirmed the new tests pass and existing call styles (side:left, from_text format:json) still compile. Two things worth a look, plus one inline nit.

Error message regression for invalid built-in option values. Because these params now resolve their argument in the enum namespace and then fall back to normal name resolution, an invalid value that happens to match (or wildcard-match) a column no longer produces the old crisp message. On main today join side:xyz gives `side` expected inner, left, right or full, but found xyz; with this PR it becomes:

   │                     ╰─── Ambiguous name
   │ Help: could be any of: that.b.xyz, this.a.xyz
   │ Note: perhaps you meant one of: inner, left, right, full

The Note: you added does carry the valid options, but leading with Ambiguous name + column candidates is more confusing than the previous message for what's usually a typo'd option. Same shape shows up in the updated append_by_wrong snapshot. Not a blocker — flagging so a maintainer can weigh whether the enum-namespace fallback should still short-circuit to the "expected one of …" error when the name isn't a valid member.

Missing CHANGELOG entry. This is a user-facing language feature but CHANGELOG.md isn't touched; the [unreleased]Features section is empty. Your comparable append by:name feature (#6046) got an entry under the previous release. Worth adding one line here.

Comment thread prqlc/prqlc/src/semantic/resolver/stmt.rs Outdated
@vanillajonathan

Copy link
Copy Markdown
Collaborator

The syntax in this PR is:

type InvoiceStatus = enum {
    Paid = 0,
    Unpaid = 1,
    Cancelled = 2
}

I think the syntax in other languages such as Rust, Python and C# is little nicer.

In Rust it would be a little cleaner:

enum InvoiceStatus {
    Paid = 0,
    Unpaid = 1,
    Canceled = 2,
}

But maybe it needs the syntax in the PR to be able to know that enum is not a pipeline function such as from and filter.

@kgutwin

kgutwin commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

The error message change is a function of the fact that now join side:xyz could theoretically be resolved to xyz as defined by a statement like let xyz = JoinSide.inner. This was the underlying motivation for the feature as described in #4499. I think the error message as it is in the PR is the most descriptive, rather than the old message which would no longer be accurate.

@kgutwin

kgutwin commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

@vanillajonathan :

In Rust it would be a little cleaner:

enum InvoiceStatus {
    Paid = 0,
    Unpaid = 1,
    Canceled = 2,
}

But maybe it needs the syntax in the PR to be able to know that enum is not a pipeline function such as from and filter.

I decided that the easiest path to implementation here was to reuse the existing type Abc = ... parser syntax and to just add a new type specifier. I don't think there's anything inherently at issue with the parser to support the enum Abc { ... } syntax, but my chumsky chops aren't that great ;) nevertheless, if you think it's worth the effort, I can give it a swag.

@prql-bot prql-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CHANGELOG entry and the expr.alias = None fix look good — the new test_enum_1b correctly pins select { status = InvoiceStatus.Paid } to 0 AS status rather than the member's own alias clobbering it. One tiny formatting nit inline.

Comment thread prqlc/prqlc/src/semantic/resolver/stmt.rs Outdated
@prql-bot

Copy link
Copy Markdown
Collaborator

Fair point — you're right that once side:xyz can legitimately resolve to a user binding (e.g. let xyz = JoinSide.inner), the old `side` expected inner, left, right or full, but found xyz message becomes misleading, since xyz genuinely might have been intended as a name. The Note: carrying the valid members is a reasonable compromise, and I agree this isn't worth blocking on.

The only residual wrinkle is presentational: leading with Ambiguous name + the column candidates buries the enum hint for what's still most often just a typo'd option. If it's ever cheap to surface the Note: perhaps you meant one of: … line first (or only when the name doesn't resolve to a real member), it'd read a bit better — but that's a nice-to-have, not a blocker.

Thanks for adding the CHANGELOG entry too.

@kgutwin

kgutwin commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

@vanillajonathan I was able to implement the cleaner enum syntax in the parser, so the syntax in this PR is now:

enum Status {
  a = 1,
  b = 2,
}

None of the tests failed so I think we're all set.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Named Unions as function parameters

3 participants