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

Incorrect type when using jsonb_agg #233

Open
timvandam opened this issue Jun 10, 2024 · 5 comments
Open

Incorrect type when using jsonb_agg #233

timvandam opened this issue Jun 10, 2024 · 5 comments

Comments

@timvandam
Copy link

timvandam commented Jun 10, 2024

Describe the bug
A clear and concise description of what the bug is.

When using jsonb_agg safeql is suggesting an incorrect TS type.

To Reproduce
Steps to reproduce the behavior:

DB setup:

await sql`
    CREATE TABLE users (
        id SERIAL PRIMARY KEY,
        public_id VARCHAR(255) UNIQUE NOT NULL,
        username VARCHAR(255) UNIQUE NOT NULL,
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );`;

await sql`
    CREATE TABLE user_authentication_methods (
        id SERIAL PRIMARY KEY,
        user_id INTEGER NOT NULL REFERENCES users (id),
        name VARCHAR(255) NOT NULL,
        -- Data depends on the authentication method
        data JSONB NOT NULL,
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );`;

await sql`
    CREATE UNIQUE INDEX user_authentication_methods_user_id_name_index
    ON user_authentication_methods (user_id, name);`;

await sql`
    CREATE FUNCTION updated_at() RETURNS TRIGGER AS $$
    BEGIN
        NEW.updated_at = NOW();
        RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;`;

await sql`
    CREATE TRIGGER users_updated_at_trigger
    BEFORE UPDATE ON users
    FOR EACH ROW
    EXECUTE PROCEDURE updated_at();`;

await sql`
    CREATE TRIGGER user_authentication_methods_updated_at_trigger
    BEFORE UPDATE ON user_authentication_methods
    FOR EACH ROW
    EXECUTE PROCEDURE updated_at();`;

Example of wrong type:

await sql<{
    id: number;
    public_id: string;
    username: string;
    created_at: Date;
    updated_at: Date;
    jsonb_agg:
        | {
              id: number | null;
              user_id: number | null;
              name: string | null;
              data: unknown | null;
              created_at: Date | null;
              updated_at: Date | null;
          }[]
        | null;
}>`
    SELECT users.*, jsonb_agg(user_authentication_methods)
    FROM users
    LEFT JOIN user_authentication_methods ON users.id = user_authentication_methods.user_id
    GROUP BY users.id
`;

// Running this with some test data gives:
// 1,test,test,2024-06-10 12:31:20.240877 +00:00,2024-06-10 12:31:20.240877 +00:00,"[{""id"": 1, ""data"": {""some_data"": 123}, ""name"": ""test1"", ""user_id"": 1, ""created_at"": ""2024-06-10T12:31:38.655757+00:00"", ""updated_at"": ""2024-06-10T12:31:38.655757+00:00""}, {""id"": 2, ""data"": {""some_other_data"": 321}, ""name"": ""test2"", ""user_id"": 1, ""created_at"": ""2024-06-10T12:31:47.674517+00:00"", ""updated_at"": ""2024-06-10T12:31:51.324642+00:00""}]"

// However, when the LEFT JOIN yields no user_authentication_methods rows we get a return value that does not match the TS type for json_agg:
await sql`
    SELECT users.*, jsonb_agg(user_authentication_methods)
    FROM users
    LEFT JOIN user_authentication_methods ON FALSE
    GROUP BY users.id;
`;

// Results in:
// 1,test,test,2024-06-10 12:31:20.240877 +00:00,2024-06-10 12:31:20.240877 +00:00,[null]

// Clearly [null] does not match json_agg's type

Expected behavior
A clear and concise description of what you expected to happen.

I would expect json_agg to have the type

({
    id: number;
    user_id: number;
    name: string;
    data: unknown; // usually any, but I override it in safeql.config.ts
    created_at: Date;
    updated_at: Date;
} | null)[]

Screenshots
If applicable, add screenshots to help explain your problem.

Desktop (please complete the following information):

Additional context
Add any other context about the problem here.

Another bonus issue I found while playing around:

await sql<{
    id: number;
    public_id: string;
    username: string;
    created_at: Date;
    updated_at: Date;
    jsonb_agg: { method: number | null }[] | null;
}>`
    SELECT users.*, jsonb_agg(json_build_object('method', user_authentication_methods))
    FROM users
    LEFT JOIN user_authentication_methods ON users.id = user_authentication_methods.user_id
    GROUP BY users.id;
`;

method is definitely not a number

@Newbie012
Copy link
Collaborator

Thanks for the detailed reproduction. I'll look into it.

@timvandam
Copy link
Author

timvandam commented Jun 10, 2024

Thanks for the detailed reproduction. I'll look into it.

Nice

I forgot to include the data in the issue, here are inserts for the data I used to test this:

insert into users (id, public_id, username, created_at, updated_at)
values  (1, 'test', 'test', '2024-06-10 12:31:20.240877 +00:00', '2024-06-10 12:31:20.240877 +00:00');

insert into user_authentication_methods (id, user_id, name, data, created_at, updated_at)
values  (1, 1, 'test1', '{"some_data": 123}', '2024-06-10 12:31:38.655757 +00:00', '2024-06-10 12:31:38.655757 +00:00'),
        (2, 1, 'test2', '{"some_other_data": 321}', '2024-06-10 12:31:47.674517 +00:00', '2024-06-10 12:31:51.324642 +00:00');

@timvandam
Copy link
Author

timvandam commented Aug 28, 2024

I've been doing some debugging to see if I can fix this. I've ended up here, and realized that this inner type should be made nullable if the argument given to json_agg may be null (such as in the case of a LEFT JOIN in my case). I am checking this with isDescribedColumnNullable, however this is not correct as I wan't to know whether the argument is nullable, while this applies to a DescribedColumn. Is there any way of doing this? E.g. I want to know whether x may be null in the case of jsonb_agg(x)

I see that context.resolver.sources contains some basic information, but it does not tell me whether tables/columns used within the query can be nullable. I'm not sure whether this is even possible.

Also, detecting whether the value in the final selection can be null (e.g. in the case above where I use GROUP BY) would require a more sophisticated way of constructing context.nonNullableColumns. For instance in my case the GROUP BY pretty much filters out any rows where jsonb_agg would have returned null, so this would need to be detected. Also seems very challenging.

I think it would make most sense to assume that jsonb_agg can contain null values whenever its argument is not the table in the FROM clause. Not 100% accurate but close enough I think

@Newbie012
Copy link
Collaborator

This is somewhat related to #210

I have unstaged changes that attempts to solve this issue by introducing a new "group" node which can be "nullable" and contain "children". This is quite a fundamental change from the way I initially wrote the ast-describe part. Another required fundamental change would be to merge the non nullable columns logic with the descriptor, since sometimes it needs additional context. Once I have enough time keep working on it, I'll send some updates. In the meantime, feel free to explore this path.

@timvandam
Copy link
Author

That's a nice approach. I think this wouldn't fully solve the GROUP BY causing any potentially null results from being omitted, but it is probably not even needed since you can just COALESCE to get rid of that null type

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

No branches or pull requests

2 participants