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

Set operations: infer type mappings from the other side #29081

Open
roji opened this issue Sep 14, 2022 · 4 comments
Open

Set operations: infer type mappings from the other side #29081

roji opened this issue Sep 14, 2022 · 4 comments

Comments

@roji
Copy link
Member

roji commented Sep 14, 2022

When performing set operations, if a constant/parameter is specified, we should infer the type mapping from the column on the other side. Currently, queries which do this frequently fail since we don't support any type/facet mismatch between the two sides (e.g. #19129).

var test = await DbContext.Foo
    .Select(f => new
    {
        FooProp = f.FooProp,
        BarProp = "SomeBar"
    })
    .Union(DbContext.Bar
        .Select(b => new
        {
            FooProp = "SomeFoo",
            BarProp = b.Bar
        }))
    .ToListAsync();
@ajcvickers
Copy link
Member

Note from triage: make sure that truncation does not occur in cases where inferred type mapping has a max length that may not accommodate values from both sides.

@roji
Copy link
Member Author

roji commented Sep 17, 2022

Did a quick investigation into this, I think the behavior corresponds to what @smitpatel said in the meeting.

When performing a union, SQL Server picks the bigger length facet from both sides. For columns, that's the length specified in the column definition, but for literals, it's the actual length of the provided literal. I saw similar behavior with the CONCAT_WS function, which returns types based on actual values provided. We can calculate this client-side for literals, but not for parameters.

Note that for PG, the length facet is simply dropped: the union between varchar(x) and varchar(y) yields varchar (which is a variable-length string with no constraint).

SQL Server
DROP TABLE IF EXISTS data;
CREATE TABLE data (
    id int IDENTITY PRIMARY KEY,
    name nvarchar(5) NULL
);

INSERT INTO data (name) VALUES ('hell');

SELECT name FROM data UNION SELECT 'goodbye'; -- shows full hell and goodbye (no truncation)
SELECT name FROM data UNION SELECT CAST('foo' AS nvarchar(3)); -- shows hell and foo

DECLARE @what sql_variant;
SELECT @what = (SELECT TOP(1) * FROM (SELECT name FROM data UNION SELECT 'goodbye') x);
SELECT SQL_VARIANT_PROPERTY(@what, 'BaseType'), SQL_VARIANT_PROPERTY(@what, 'MaxLength');
-- returns nvarchar with length 14. 14 is 7 (the length of goodbye) times 2 (nvarchar).

DECLARE @what sql_variant;
SELECT @what = (SELECT TOP(1) * FROM (SELECT name FROM data UNION SELECT CAST('foo' AS nvarchar(3))) x);
SELECT SQL_VARIANT_PROPERTY(@what, 'BaseType'), SQL_VARIANT_PROPERTY(@what, 'MaxLength');
-- returns nvarchar with length 10. 10 is 5 (from the column definition) times 2 (nvarchar).
PostgreSQL
DROP TABLE IF EXISTS data;
CREATE TABLE data (
    id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    name varchar(5)
);

INSERT INTO data (name) VALUES ('hell');

SELECT name FROM data UNION SELECT 'goodbye'; -- shows full hell and goodbye (no truncation)
SELECT name FROM data UNION SELECT 'foo'::varchar(3); -- shows hell and foo

SELECT pg_typeof((SELECT * FROM (SELECT name FROM data UNION SELECT 'goodbye') x LIMIT 1));
-- shows character varying, no length facet

SELECT pg_typeof((SELECT * FROM (SELECT name FROM data UNION SELECT 'foo'::varchar(3)) x LIMIT 1));
-- also shows character varying, no length facet. So doing union over varchar(x) and varchar(y) returns varchar
-- with no length

@ioaznnis
Copy link

Also not work for numeric type.

var test = await DbContext.Foo
    .Select(f => new
    {
        FooPropFirst = 0,
        FooPropSecond = f.FooProp,
    })
    .Union(DbContext.Foo
        .Select(b => new
        {
             FooPropFirst = f.FooProp,
             FooPropSecond = 0,
        }))
    .ToListAsync();

But work with workaround for PostgreSQL numeric:

var test = await DbContext.Foo
    .Select(f => new
    {
        FooPropFirst = f.FooProp * 0,
        FooPropSecond = f.FooProp * 1,
    })
    .Union(DbContext.Foo
        .Select(b => new
        {
             FooPropFirst = f.FooProp * 1,
             FooPropSecond = f.FooProp * 0,
        }))
    .ToListAsync();

@ajcvickers
Copy link
Member

Related to #15586

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

No branches or pull requests

3 participants