Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* Extend the `=` adjacent to an interpolated string fix to the verbatim (`=$@"…"`, `=@$"…"`) and extended multi-dollar (`=$$"""…"""`) interpolated-string forms. ([Issue #16696](https://github.com/dotnet/fsharp/issues/16696), [PR #19984](https://github.com/dotnet/fsharp/pull/19984))
* Preserve type abbreviations (`string`, user-defined aliases) in the refined type of bindings introduced after a `| null` pattern in a `match` expression. ([Issue #19646](https://github.com/dotnet/fsharp/issues/19646), [PR #19745](https://github.com/dotnet/fsharp/pull/19745))
* Fix attributes on return type of unparenthesized tuple methods being silently dropped from IL. ([Issue #462](https://github.com/dotnet/fsharp/issues/462), [PR #19714](https://github.com/dotnet/fsharp/pull/19714))
* Fix enum values losing their type when used in a custom attribute argument of type `obj` (they were stored as the underlying integer instead of the enum). ([Issue #995](https://github.com/dotnet/fsharp/issues/995), [PR #19975](https://github.com/dotnet/fsharp/pull/19975))
* Fix false-positive nullness warning (FS3261) when pattern matching narrows nullness inside seq/list/array comprehensions. ([Issue #19644](https://github.com/dotnet/fsharp/issues/19644), [PR #19743](https://github.com/dotnet/fsharp/pull/19743))
* Fix internal error FS0073 "Undefined or unsolved type variable" in IlxGen when nested inline SRTP functions with multiple overloads leave unsolved typars in the non-witness codegen path. ([Issue #19709](https://github.com/dotnet/fsharp/issues/19709), [PR #19710](https://github.com/dotnet/fsharp/pull/19710))
* Fix NRE when calling virtual Object methods on value types through inline SRTP functions. ([Issue #8098](https://github.com/dotnet/fsharp/issues/8098), [PR #19511](https://github.com/dotnet/fsharp/pull/19511))
Expand Down
66 changes: 41 additions & 25 deletions src/Compiler/AbstractIL/il.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,7 @@ type ILAttribElem =
| Type of ILType option
| TypeRef of ILTypeRef option
| Array of ILType * ILAttribElem list
| Enum of enumType: ILType * value: ILAttribElem

type ILAttributeNamedArg = string * ILType * bool * ILAttribElem

Expand Down Expand Up @@ -4897,6 +4898,8 @@ let rec encodeCustomAttrElemTypeForObject x =
| ILAttribElem.Single _ -> [| et_R4 |]
| ILAttribElem.Double _ -> [| et_R8 |]
| ILAttribElem.Array(elemTy, _) -> [| yield et_SZARRAY; yield! encodeCustomAttrElemType elemTy |]
// An enum boxed in 'object' is encoded as 0x55 followed by the enum type's qualified name.
| ILAttribElem.Enum(enumTy, _) -> encodeCustomAttrElemType enumTy

let parseILVersion (vstr: string) =
// matches "v1.2.3.4" or "1.2.3.4". Note, if numbers are missing, returns -1 (not 0).
Expand Down Expand Up @@ -4994,6 +4997,25 @@ let rec decodeCustomAttrElemType bytes sigptr x =
mkILArr1DTy elemTy, sigptr
| x when x = 0x50uy -> PrimaryAssemblyILGlobals.typ_Type, sigptr
| x when x = 0x51uy -> PrimaryAssemblyILGlobals.typ_Object, sigptr // SERIALIZATION_TYPE_TAGGED_OBJECT (ECMA-335 II.23.3)
| x when x = 0x55uy ->
// SERIALIZATION_TYPE_ENUM (ECMA-335 II.23.3): the enum type's qualified name follows.
// Occurs e.g. when an enum is boxed into an 'object'-typed argument.
let qualifiedName, sigptr = sigptr_get_serstring bytes sigptr

let unqualifiedName, rest =
let pieces = qualifiedName.Split ','

if pieces.Length > 1 then
pieces[0], Some(String.concat "," pieces[1..])
else
pieces[0], None

let scoref =
match rest with
| Some aname -> ILScopeRef.Assembly(ILAssemblyRef.FromAssemblyName(AssemblyName aname))
| None -> PrimaryAssemblyILGlobals.primaryAssemblyScopeRef

ILType.Value(mkILNonGenericTySpec (mkILTyRef (scoref, unqualifiedName))), sigptr
| _ -> failwithf "decodeCustomAttrElemType ilg: unrecognized custom element type: %A" x

/// Given a custom attribute element, encode it to a binary representation according to the rules in Ecma 335 Partition II.
Expand Down Expand Up @@ -5024,6 +5046,8 @@ let rec encodeCustomAttrPrimValue c =
for elem in elems do
yield! encodeCustomAttrPrimValue elem
|]
// The enum type is captured separately (in the type tag); the value is the underlying integer.
| ILAttribElem.Enum(_, value) -> encodeCustomAttrPrimValue value

and encodeCustomAttrValue ty c =
match ty, c with
Expand Down Expand Up @@ -5370,7 +5394,15 @@ let decodeILAttribData (ca: ILAttribute) =
ILAttribElem.Null, sigptr
else
let ty, sigptr = decodeCustomAttrElemType bytes sigptr et
parseVal ty sigptr
let v, sigptr = parseVal ty sigptr
// Only a genuine enum (the 0x55 tag) is wrapped as ILAttribElem.Enum, so it
// re-encodes with its 0x55 enum tag (e.g. during static linking). Boxed primitives
// (et_I4, et_BOOLEAN, ...) also decode to an ILType.Value here but must be left as
// their primitive element. See https://github.com/dotnet/fsharp/issues/995.
if et = 0x55uy then
ILAttribElem.Enum(ty, v), sigptr
else
v, sigptr
| ILType.Array(shape, elemTy) when shape = ILArrayShape.SingleDimensional ->
let n, sigptr = sigptr_get_i32 bytes sigptr

Expand All @@ -5386,7 +5418,11 @@ let decodeILAttribData (ca: ILAttribute) =

let elems, sigptr = parseElems [] n sigptr
ILAttribElem.Array(elemTy, elems), sigptr
| ILType.Value _ -> (* assume it is an enumeration *)
| ILType.Value _ ->

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Encode/decode width is asymmetric for non-int32 boxed enums. The new encode path (GenAttribArgILAttribElem.Enum(enumIlTy, underlyingElem)encodeCustomAttrPrimValue) emits the underlying value at its true width — 8 bytes for an int64 enum like the LongEnum added in EnumValueAsObjectArg01.fs. But when that blob is read back through decodeILAttribData, a boxed enum decodes to ILType.Value <enumName> (via the 0x55 case) and lands in this fallback, which unconditionally reads int32 (4 bytes).

Consequences for any non-int32 boxed enum:

  • the decoded value is truncated/wrong, and
  • sigptr advances 4 bytes instead of 8, desyncing the parse of any following named argument (silent blob corruption).

The LongEnum test only exercises the CLR's metadata reader at runtime, so it doesn't cover this path. The comment here acknowledges the int32 assumption but understates the impact — the PR description claims decode/encode are "kept symmetric so static linking preserves it," which doesn't hold for int64/byte/int16/… enums. Consider reading the value at the enum's underlying width (resolving it from the 0x55 type) or, at minimum, not silently advancing by the wrong amount.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think this is a pre-existing, documented assumption of decodeILAttribData

/// Not all custom attribute data can be decoded without binding types.  In particular
/// enums must be bound in order to discover the size of the underlying integer.
/// The following assumes enums have size int32.

// Assume an enumeration. Note: the underlying integer width is not present in the
// blob, so this reads it as int32. Enums with a non-int32 underlying type (byte,
// int16, int64, ...) are therefore not read correctly here; resolving that would
// require materializing the enum type, which this blob parser does not do.
let n, sigptr = sigptr_get_i32 bytes sigptr
ILAttribElem.Int32 n, sigptr
| _ -> failwith "decodeILAttribData: attribute data involves an enum or System.Type value"
Expand All @@ -5409,29 +5445,9 @@ let decodeILAttribData (ca: ILAttribute) =
let isPropByte, sigptr = sigptr_get_u8 bytes sigptr
let isProp = (int isPropByte = 0x54)
let et, sigptr = sigptr_get_u8 bytes sigptr
// We have a named value
let ty, sigptr =
if ( (* 0x50 = (int et) || *) 0x55 = (int et)) then
let qualified_tname, sigptr = sigptr_get_serstring bytes sigptr

let unqualified_tname, rest =
let pieces = qualified_tname.Split ','

if pieces.Length > 1 then
pieces[0], Some(String.concat "," pieces[1..])
else
pieces[0], None

let scoref =
match rest with
| Some aname -> ILScopeRef.Assembly(ILAssemblyRef.FromAssemblyName(AssemblyName aname))
| None -> PrimaryAssemblyILGlobals.primaryAssemblyScopeRef

let tref = mkILTyRef (scoref, unqualified_tname)
let tspec = mkILNonGenericTySpec tref
ILType.Value tspec, sigptr
else
decodeCustomAttrElemType bytes sigptr et
// We have a named value. The type tag (including the 0x55 enum form) is decoded by
// decodeCustomAttrElemType.
let ty, sigptr = decodeCustomAttrElemType bytes sigptr et

let nm, sigptr = sigptr_get_serstring bytes sigptr
let v, sigptr = parseVal ty sigptr
Expand Down
5 changes: 5 additions & 0 deletions src/Compiler/AbstractIL/il.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,11 @@ type ILAttribElem =
| Type of ILType option
| TypeRef of ILTypeRef option
| Array of ILType * ILAttribElem list
/// Represents an enum value together with its enum type. Used when an enum is stored in a
/// custom-attribute argument of type 'object', so the enum type is preserved in the encoded
/// blob (ECMA-335 II.23.3) instead of being collapsed to its underlying integer. The second
/// element is the underlying integer value (e.g. ILAttribElem.Int32).
| Enum of enumType: ILType * value: ILAttribElem

/// Named args: values and flags indicating if they are fields or properties.
type ILAttributeNamedArg = string * ILType * bool * ILAttribElem
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/AbstractIL/ilmorph.fs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ let rec celem_ty2ty f celem =
| ILAttribElem.Type(Some ty) -> ILAttribElem.Type(Some(f ty))
| ILAttribElem.TypeRef(Some tref) -> ILAttribElem.TypeRef(Some (f (mkILBoxedType (mkILNonGenericTySpec tref))).TypeRef)
| ILAttribElem.Array(elemTy, elems) -> ILAttribElem.Array(f elemTy, List.map (celem_ty2ty f) elems)
| ILAttribElem.Enum(enumTy, value) -> ILAttribElem.Enum(f enumTy, celem_ty2ty f value)
| _ -> celem

let cnamedarg_ty2ty f ((nm, ty, isProp, elem): ILAttributeNamedArg) = (nm, f ty, isProp, celem_ty2ty f elem)
Expand Down
2 changes: 2 additions & 0 deletions src/Compiler/Checking/AttributeChecking.fs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ let rec private evalILAttribElem elem =
| ILAttribElem.Double x -> box x
| ILAttribElem.Null -> null
| ILAttribElem.Array (_, a) -> box [| for i in a -> evalILAttribElem i |]
// An enum value: evaluate to its underlying integer value (the enum type itself is not materialized here).
| ILAttribElem.Enum (_, value) -> evalILAttribElem value
// TODO: typeof<..> in attribute values
| ILAttribElem.Type (Some _t) -> fail()
| ILAttribElem.Type None -> null
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Checking/NicePrint.fs
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,7 @@ module PrintTypes =
| ILAttribElem.TypeRef (Some ty) ->
LeftL.keywordTypedefof ^^ SepL.leftAngle ^^ PrintIL.layoutILTypeRef denv ty ^^ RightL.rightAngle
| ILAttribElem.TypeRef None -> emptyL
| ILAttribElem.Enum (_, value) -> layoutILAttribElement denv value

and layoutILAttrib denv (ty, args) =
let argsL = bracketL (sepListL RightL.comma (List.map (layoutILAttribElement denv) args))
Expand Down
15 changes: 15 additions & 0 deletions src/Compiler/CodeGen/IlxGen.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10407,6 +10407,21 @@ and GenAttribArg amap (g: TcGlobals) eenv x (ilArgTy: ILType) =
// Detect 'null' used for an array argument
| Expr.Const(Const.Zero, _, _), ILType.Array _ -> ILAttribElem.Null

// An enum value stored into an 'object'-typed argument must keep its enum type in the
// custom-attribute blob (ECMA-335 II.23.3), otherwise it round-trips as the underlying
// integer (e.g. 'Prop = MyEnum.B' surfaces as boxed int32). See
// https://github.com/dotnet/fsharp/issues/995. The enum type is carried alongside the
// underlying integer value, which is computed by recursing with the underlying IL type.
| Expr.Const(c, m, ty), _ when ilArgTy.TypeSpec.Name = "System.Object" && isEnumTy g ty ->
let enumIlTy = GenType amap m eenv.tyenv ty
let underlyingTy = underlyingTypeOfEnumTy g ty
let underlyingIlTy = GenType amap m eenv.tyenv underlyingTy

let underlyingElem =
GenAttribArg amap g eenv (Expr.Const(c, m, underlyingTy)) underlyingIlTy

ILAttribElem.Enum(enumIlTy, underlyingElem)

// Detect standard constants
| Expr.Const(c, m, ty), _ ->
let tynm = ilArgTy.TypeSpec.Name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,48 @@ module CustomAttributes_Basic =
|> verifyCompileAndRun
|> shouldSucceed

// Regression for https://github.com/dotnet/fsharp/issues/995
[<Theory; Directory(__SOURCE_DIRECTORY__, Includes=[|"EnumValueAsObjectArg01.fs"|])>]
let ``EnumValueAsObjectArg01_fs`` compilation =
compilation
|> verifyCompileAndRun
|> shouldSucceed

// Cross-language: the same scenario as EnumValueAsObjectArg01.fs, but with the enum and the
// attribute defined in C#. See https://github.com/dotnet/fsharp/issues/995.
[<Fact>]
let ``Enum defined in C# used in an F# attribute arg of type obj keeps its type`` () =
let csLib =
CSharp """
namespace CSharpLib
{
public enum MyEnum { A = 1, B = 2 }

[System.AttributeUsage(System.AttributeTargets.All)]
public class MyAttribute : System.Attribute
{
public object Prop { get; set; }
}
}
"""
|> withName "CSharpLib"

FSharp """
module Test
open System
open CSharpLib

[<My(Prop = MyEnum.B)>]
type MyClass = class end

let prop = (typeof<MyClass>.GetCustomAttributes(false)[0] :?> MyAttribute).Prop
if prop.GetType() <> typeof<MyEnum> then failwith "enum type was lost"
if Convert.ToString(prop, Globalization.CultureInfo.InvariantCulture) <> "B" then failwith "expected \"B\""
"""
|> withReferences [csLib]
|> compileExeAndRun
|> shouldSucceed

// SOURCE=E_AttributeApplication01.fs # E_AttributeApplication01.fs
[<Theory; Directory(__SOURCE_DIRECTORY__, Includes=[|"E_AttributeApplication01.fs"|])>]
let ``E_AttributeApplication01_fs`` compilation =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// #Conformance #DeclarationElements #Attributes
// Regression test for https://github.com/dotnet/fsharp/issues/995
// An enum assigned to an attribute argument of type 'obj' must keep its enum type in the
// emitted metadata, instead of being stored as the underlying int32.

open System

type MyAttribute() =
inherit Attribute()
let mutable prop : obj = null
member _.Prop
with get () : obj = prop
and set (value: obj) = prop <- value

type MyEnum =
| A = 1
| B = 2

// An enum with a non-int32 underlying type, to exercise the encoded value width.
type LongEnum =
| P = 1L
| Q = 2L

[<My(Prop = MyEnum.B)>]
type MyClass = class end

[<My(Prop = LongEnum.Q)>]
type MyClassLong = class end

let propOf<'T> () = (typeof<'T>.GetCustomAttributes(false)[0] :?> MyAttribute).Prop

let intProp = propOf<MyClass> ()
if intProp.GetType() <> typeof<MyEnum> then failwith "MyEnum type was lost"
if Convert.ToString(intProp, Globalization.CultureInfo.InvariantCulture) <> "B" then failwith "expected \"B\""

let longProp = propOf<MyClassLong> ()
if longProp.GetType() <> typeof<LongEnum> then failwith "LongEnum type was lost"
if Convert.ToString(longProp, Globalization.CultureInfo.InvariantCulture) <> "Q" then failwith "expected \"Q\""
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ FSharp.Compiler.AbstractIL.IL+ILAttribElem+Char: Char Item
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Char: Char get_Item()
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Double: Double Item
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Double: Double get_Item()
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Enum: ILAttribElem get_value()
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Enum: ILAttribElem value
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Enum: ILType enumType
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Enum: ILType get_enumType()
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Int16: Int16 Item
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Int16: Int16 get_Item()
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Int32: Int32 Item
Expand All @@ -161,6 +165,7 @@ FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Bool
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Byte
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Char
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Double
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Enum
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Int16
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Int32
FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Int64
Expand Down Expand Up @@ -192,6 +197,7 @@ FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsBool
FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsByte
FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsChar
FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsDouble
FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsEnum
FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsInt16
FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsInt32
FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsInt64
Expand All @@ -209,6 +215,7 @@ FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsBool()
FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsByte()
FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsChar()
FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsDouble()
FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsEnum()
FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsInt16()
FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsInt32()
FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsInt64()
Expand All @@ -226,6 +233,7 @@ FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttr
FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttribElem+Byte
FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttribElem+Char
FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttribElem+Double
FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttribElem+Enum
FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttribElem+Int16
FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttribElem+Int32
FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttribElem+Int64
Expand All @@ -243,6 +251,7 @@ FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewBool(Boolean)
FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewByte(Byte)
FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewChar(Char)
FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewDouble(Double)
FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewEnum(ILType, ILAttribElem)
FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewInt16(Int16)
FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewInt32(Int32)
FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewInt64(Int64)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ type E = Microsoft.FSharp.Quotations.Expr;;
type StaticIndexedPropertyTest() =
static member IdxProp with get (n : int) = n + 1

type QuotationEnum =
| A = 1
| B = 2

module Check =
let argumentException f =
let mutable ex = false
Expand Down Expand Up @@ -103,6 +107,16 @@ type FSharpQuotationsTests() =
| NewTuple [ Value(:? int as i, _) ; Value(:? string as s, _) ] when i = 1 && s = "" -> ()
| _ -> Assert.Fail()

[<Fact>]
member x.``Quotation of an enum value preserves the enum type`` () =
// Related to https://github.com/dotnet/fsharp/issues/995: an enum literal is quoted as a
// Value node carrying the enum type, not the bare underlying integer.
match <@ QuotationEnum.B @> with
| Value(v, t) ->
Assert.Equal(typeof<QuotationEnum>, t)
Assert.Equal(box QuotationEnum.B, v)
| _ -> Assert.Fail()

[<Fact>]
member x.``NewTuple literal should not be recognized by NewStructTuple active pattern`` () =
match <@ (1, "") @> with
Expand Down
Loading