Skip to content

Refactor type handler hierarchy for range/array#2042

Closed
roji wants to merge 1 commit intonpgsql:devfrom
roji:no-recursive-types
Closed

Refactor type handler hierarchy for range/array#2042
roji wants to merge 1 commit intonpgsql:devfrom
roji:no-recursive-types

Conversation

@roji
Copy link
Member

@roji roji commented Jul 4, 2018

Type handlers are factories of range and array handlers over their types. In other words, to create a range handler over int, one calls the CreateRangeHandler() on the int handler.

Previously, all type handlers had CreateRangeHandler() and CreateArrayHandler(), but on RangeHandler and ArrayHandler these threw NotSupportedException, respectively. Aside from being a bit ugly, this created issues for AOT compilers (corert, UWP) which failed on infinite potential type recursion (range of range of range...).

Changed the type handler hierarchy to indicate via interfaces which handlers support ranges and arrays.

Fixes #2040, #1861


Note that this changes the plugin API, so all plugins will need to be modified accordingly and released. Aside from this it may be low-risk enough for inclusion in 4.0.2.

@YohDeadfall what do you think?

Type handlers are factories of range and array handlers over their
types. In other words, to create a range handler over int, one
calls the CreateRangeHandler() on the int handler.

Previously, all type handlers had CreateRangeHandler() and
CreateArrayHandler(), but on RangeHandler and ArrayHandler these threw
NotSupportedException, respectively. Aside from being a bit ugly, this
created issues for AOT compilers (corert, UWP) which failed on infinite
potential type recursion (range of range of range...).

Changed the type handler hierarchy to indicate via interfaces which
handlers support ranges and arrays.

Fixes npgsql#2040, npgsql#1861
@roji roji added the 🪲 bug label Jul 4, 2018
@roji roji added this to the 4.0.2 milestone Jul 4, 2018
@roji roji requested a review from YohDeadfall July 4, 2018 21:37
/// Type handlers can support additional types by implementing <see cref="INpgsqlTypeHandler{T}"/>.
/// </typeparam>
public abstract class NpgsqlBaseTypeHandler<TDefault> : NpgsqlTypeHandler<TDefault>,
INpgsqlRangeHandlerFactory, INpgsqlArrayHandlerFactory
Copy link
Contributor

Choose a reason for hiding this comment

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

It's overly complicated. Maybe move the implementation of CreateArrayHandler and CreateRangeHandler to NpgsqlBaseTypeHandler<TDefault> from NpgsqlTypeHandler<TDefault>? Would it be enough to fix the bug?

Copy link
Member Author

Choose a reason for hiding this comment

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

Right now Create{Array,Range}Handler() are on non-generic NpgsqlTypeHandler, not on generic NpgsqlTypeHandler<T>. But moving these two methods to NpgsqlBaseTypeHandler<TDefault> is exactly what this PR does...

If by "overly complicated" you mean adding the two interfaces, I did this mainly because we have at least some cases where we need to support one and not the other. For example, RangeHandler itself needs to be able to produce an array (array of ranges) but it absolutely shouldn't produce a range (which is what triggered this whole issue).

The two interfaces don't seem to be that bad, very few classes need to be aware of them... Do they really bother you?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm talking about the implementation, not the definition. The last one in NpgsqlTypeHandler (: If I understand you correctly now, overriding the CreateRangeHandler method in RangeHandler should fix the issue because it breaks the circular dependency. So even NpgsqlBaseTypeHandler<TDefault> isn't required. Right?

I'm not happy with such change because it breaks public API. It's not a good idea for 4.0.2.

Copy link
Member Author

@roji roji Jul 4, 2018

Choose a reason for hiding this comment

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

I'm talking about the implementation, not the definition. The last one in NpgsqlTypeHandler (: If I understand you correctly now, overriding the CreateRangeHandler method in RangeHandler should fix the issue because it breaks the circular dependency. So even NpgsqlBaseTypeHandler isn't required. Right?

I don't understand... In the current Npgsql (before this PR) both the methods are on NpgsqlTypeHandler<T> and are overridden in RangeHandler like you suggest above. This doesn't fix the issue, because the AOT compilers analyzing Npgsql don't know that an exception is thrown when CreateRangeHandler() is called seen specifically on RangeHandler - this is the problem we're trying to solve. As far as I can understand the only way is to not have CreateRangeHandler() at all on RangeHandler...

Maybe I'm misunderstanding you, it may be easier for you to post some code (or even submit a different PR)...

Regarding the public API breakage... I don't consider the plugin API to be the same as other user-facing APIs: I think we should have much more freedom to break that as we need to, in order to keep our type handler model clean and evolve it based on needs. I see it a bit like EF Core's internal namespace: it's technically public but they make no guarantees that it won't change from release to release. Having said that, it isn't critical to merge this before 4.1.

(It's true that this isn't really documented though - we should improve that).

Copy link
Contributor

Choose a reason for hiding this comment

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

because the AOT compilers analyzing Npgsql don't know that an exception is thrown when CreateRangeHandler() is called specifically on RangeHandler - this is the problem we're trying to solve.

Ah so... Will play with the issue and your PR tomorrow.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like the current approach because:

  1. it complicates the code,
  2. the code works slower because of casting to an INpgsqlRangeHandlerFactory and checking that the value isn't null.

So it would be much better just to alter the inheritance for RangeHandler, I think, like it was done for ArrayHandler<TElement>. The code below doesn't use interfaces and casts at all, but it breaks the circular dependency.

public abstract class NpgsqlTypeHandler
{
    public abstract ArrayHandler CreateArrayHandler(PostgresType arrayBackendType);
    public abstract RangeHandler CreateRangeHandler(PostgresType rangeBackendType);
}

public abstract class ArrayHandler : NpgsqlTypeHandler { }
public sealed class ArrayHandler<TElement> : ArrayHandler { }

public abstract class RangeHandler : NpgsqlTypeHandler { }
public sealed class RangeHandler<TElement> : RangeHandler { }

public abstract class NpgsqlTypeHandler<TDefault> : NpgsqlTypeHandler
{
    public override ArrayHandler CreateArrayHandler(PostgresType arrayBackendType)
        => new ArrayHandler<TDefault>(this) { PostgresType = arrayBackendType };

    public override RangeHandler CreateRangeHandler(PostgresType rangeBackendType)
        => new RangeHandler<TDefault>(this) { PostgresType = rangeBackendType };
}

What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't like the current approach because:

  1. it complicates the code,

That's true, it's an additional type in the type handler hierarchy (although it makes the hierarchy correspond more to PostgreSQL types - base types vs. others etc.).

  1. the code works slower because of casting to an INpgsqlRangeHandlerFactory and checking that the value isn't null.

Are you referring to this bind-time check? If so then speed really isn't an issue, this is only a setup phase that happens when the physical connection is first set up (or when reloading happens)... So this is completely negligible (unless I misunderstood you).

So it would be much better just to alter the inheritance for RangeHandler, I think, like it was done for ArrayHandler. The code below doesn't use interfaces and casts at all, but it breaks the circular dependency.

I like the idea, but I don't see how you would actually invoke CreateRangeHandler() and CreateArrayHandler(). These methods are called at bind-time (again, in this method), but at that point we only have an NpgsqlTypeHandler, and not an NpgsqlTypeHandler<T>...

In general, we're only in "generic" mode when reading or writing actual values (as part of regular connection use); during setup time we're in non-generic mode.

If I've misunderstood you, maybe a quick PR (even WIP) could help me understand.

Copy link
Contributor

Choose a reason for hiding this comment

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

Opened #2060 to continue our discussion.

@roji
Copy link
Member Author

roji commented Jul 21, 2018

Superceded by #2060.

@roji roji closed this Jul 21, 2018
@roji roji deleted the no-recursive-types branch March 3, 2019 10:48
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.

2 participants