Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix enums used as parameter for string columns #2622

Merged
merged 5 commits into from
Oct 31, 2022

Conversation

csharper2010
Copy link
Contributor

@csharper2010 csharper2010 commented Nov 25, 2020

A special treatment of enums mapped on string columns is required, because all reasons for checking string lengths or streamlining the length of parameters of queries on string columns to the length of the column for SQL Server
are also valid for enums, since they are sometimes by convention used to model the possible values of a string column.

Fixes #2621.

Special treatment of enums mapped on string columns because all reasons
for checking string lengths or streamlining the length of parameters of
queries on string columns to the length of the column for SQL Server
are also valid for enums because they are sometimes by convention used
to model the possible values of a string column
hazzik
hazzik previously approved these changes Nov 30, 2020
@csharper2010
Copy link
Contributor Author

Sorry for not seeing this... Adding the logic directly to the AbstractStringType is surely better.

One question: should we make it more explicit for Enums because the Convert.ToType is very generic. Might help in some cases but might also cover real bugs in an application.

So handle Enums explicitly like I did in the Dialect classes?
if (value is Enum) value = Enum.GetName(value.GetType(), value);

Or leave it as is?

@hazzik
Copy link
Member

hazzik commented Dec 9, 2020

Or leave it as is?

I don't know.

{
var query = s.CreateQuery(
@"SELECT Name FROM NHibernate.Test.NHSpecificTest.GH2621Enum.ClassWithString ROOT WHERE ROOT.Kind = :kind");
query.SetParameter("kind", Kind.SomeKind);
Copy link
Member

@hazzik hazzik Dec 9, 2020

Choose a reason for hiding this comment

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

Why not using one of the following methods?

/* 1 */ query.SetParameter("kind", Kind.SomeKind, NHibernateUtil.Enum(typeof(Kind)));
/* 2 */ query.SetEnum("kind", Kind.SomeKind);
/* 3 */ query.SetParameter("kind", Kind.SomeKind.ToString());

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because it's an existing code base where I cannot check all calls to SetParameter.

I would say that the change is a very specific one that does not hurt.

@@ -58,13 +58,16 @@ public override void Set(DbCommand cmd, object value, int index, ISessionImpleme
{
var parameter = cmd.Parameters[index];

if (value is Enum)
value = Enum.GetName(value.GetType(), value);
Copy link
Member

Choose a reason for hiding this comment

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

Why not just value = value.ToString()?

@gliljas
Copy link
Member

gliljas commented Dec 14, 2020

If the objective is to solve a problem in an existing code base, why not just change the string type used, to one where this modification has been applied? That would require fixing the mapping, not all calls to SetParameter.

Using Enums as string constants is bad practice.

Copy link
Member

@fredericDelaporte fredericDelaporte left a comment

Choose a reason for hiding this comment

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

I am not in favor of this change. It is too specific, and easy to manage on user side by deriving a custom type as suggested by Gunnar.

@hazzik
Copy link
Member

hazzik commented Feb 17, 2021

@fredericDelaporte what about changes as in e8c8a3b (minus null check)?

@fredericDelaporte
Copy link
Member

That sounds better to me, why not accepting this e8c8a3b (minus null check).

@hazzik
Copy link
Member

hazzik commented Feb 24, 2021

@fredericDelaporte @gliljas please check. This change is consistent with how some other types handle parameters, for example:

public override void Set(DbCommand rs, object value, int index, ISessionImplementor session)
{
rs.Parameters[index].Value = Convert.ToInt32(value);
}

@csharper2010
Copy link
Contributor Author

csharper2010 commented Feb 26, 2021

@fredericDelaporte @gliljas please check. This change is consistent with how some other types handle parameters, for example:

public override void Set(DbCommand rs, object value, int index, ISessionImplementor session)
{
rs.Parameters[index].Value = Convert.ToInt32(value);
}

Sorry for picking up that discussion that late but to be honest, I fear that the general approach using Convert.ToString might cover programming bugs of app developers. Convert.ToString(new object()) leads to "System.Object", the result of Convert.ToString(1.5m) is locale-dependent.

On the one hand I am interested in a solution but that might be a bit risky. That's why I originally had the enum handled specifically because it has a natural way of being expressed as string (admittedly there are the edge cases with integers not being represented as enum string so my solution with Enum.GetName might not be perfect, e.g. it leads to null for (FileAccess)0 whereas Convert.ToString((FileAccess)0) gives the string "0").

Maybe we should guard the Convert.ToString with a list of defined cases where conversion to string is safe in some way.

var stringValue = value as string;
if (stringValue == null && value != null)
{
    if (value is Enum || value is xxx || value is yyy)
        stringValue = Convert.ToString(value);
    else
        throw new ArgumentException();
}

@@ -57,14 +57,15 @@ public AbstractStringType(SqlType sqlType)
public override void Set(DbCommand cmd, object value, int index, ISessionImplementor session)
{
var parameter = cmd.Parameters[index];
var stringValue = Convert.ToString(value);
Copy link
Member

@bahusoid bahusoid Feb 16, 2022

Choose a reason for hiding this comment

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

I fear that the general approach using Convert.ToString might cover programming bugs of app developers. Convert.ToString(new object()) leads to "System.Object", the result of Convert.ToString(1.5m) is locale-dependent.

I totally agree with @csharper2010 on this. We should not silently convert parameters with incorrect types to string. I see it as a source of hard to find bugs.

If you think it's useful let's do it for enums only but let's not make it a general fix.

@csharper2010
Copy link
Contributor Author

Hi again,

I would like to pick that up again...
Should I revert the change back to the original one with just handling Enums? Do I have a chance to get that landed? There will not be a risk of regression bugs if we make it small and there is also no risk of covering application bugs as with Convert.ToString().

Thanks

@hazzik
Copy link
Member

hazzik commented Sep 8, 2022

Rebased and dropped all other commits.

@hazzik
Copy link
Member

hazzik commented Sep 8, 2022

Reverted the 'fix' as I'm trying to understand what we are fixing here.

@hazzik hazzik changed the title Fixes #2621 Fix enums used as parameter for string columns Oct 27, 2022
@hazzik
Copy link
Member

hazzik commented Oct 27, 2022

Ok, this was dragging for a long time. We need to decide what to do. Just for context enum might not have a name (!) and Enum.GetName(...) will return null. It is the case for [Flags] enums and enums with out of bound values

Returns
String
A string containing the name of the enumerated constant in enumType whose value is value; or null if no such constant is found.

These unnamed enum values however, have a valid ToString() representation that can be parsed back.

using System;

Print(E.A | E.B);             // -> Name: ''; ToString: 'A, B'
Print(Enum.Parse<E>("A, B")); // -> Name: ''; ToString: 'A, B'

Print((E) 100);               // -> Name: ''; ToString: '100'
Print(Enum.Parse<E>("100"));  // -> Name: ''; ToString: '100'

void Print(E e) => Console.WriteLine($"Name: '{Enum.GetName(e.GetType(), e)}'; ToString: '{e.ToString()}'");

[Flags]
enum E
{
	A = 1 << 0,
	B = 1 << 1,
	C = 1 << 2
}

So, with the proposed solution, it is possible to have the value lost in transit, and this is NOT a desirable outcome.

I personally do not want to add a workaround for clearly invalid mapping. However, I'm open to accept the special treatment for the enums in a form of ToString(...) call if @nhibernate/core agrees:

if (value is Enum e)
      value = e.ToString(); // EDIT: removed CultureInfo.InvariantCulture parameter because method is obsolete

@hazzik

This comment was marked as off-topic.

hazzik
hazzik previously approved these changes Oct 27, 2022
bahusoid
bahusoid previously approved these changes Oct 27, 2022
hazzik
hazzik previously approved these changes Oct 27, 2022
@hazzik hazzik dismissed stale reviews from bahusoid and themself via c4c7716 October 28, 2022 03:09
@hazzik hazzik enabled auto-merge (squash) October 30, 2022 23:00
@hazzik hazzik merged commit e5ad951 into nhibernate:master Oct 31, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Regression bug with enums used as parameter for string column
6 participants