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

Design Discussion: How to use CTE table name in JOIN and WHERE clause #163

Closed
huandu opened this issue Aug 26, 2024 · 3 comments
Closed

Comments

@huandu
Copy link
Owner

huandu commented Aug 26, 2024

In current design, if we call With() method on SelectBuilder, CTE table names will be automatically added to the FROM clause in SELECT statement. This behavior is not expected sometimes if we want to use CTE table in JOIN or WHERE clause, not in FROM. However, there is no way to change this behavior. We need to design new API or do some break changes to fix it.

Based on comments 1 and 2 by @arikkfir in #161, I create this issue to discuss how to solve this issue. Feel free to join this discussion and share your thoughts.

I have two possible solutions. My personal preference is the solution 1.

Solution 1: Keep CTETable() as it is, and add new method CTEQuery() to build CTE query without any side effect

CTETableBuilder will be renamed to CTEQueryBuilder, which is just designed to build CTE query without any side effect, e.g. adding its name in FROM clause. To preserve current behavior, CTETable() will return *CTEQueryBuilder with a flag indicating CTEBuilder to add this CTE query name to FROM clause.

Ideally, this change will not break any existing API, as the struct CTETableBuilder is not expected to be used directly by any user. To ensure backward compatibility, I'll add a type alias type CTETableBuilder = CTEQueryBuilder.

Following is a sample using this new API.

b := With(
    // CTEQuery will NOT be added to FROM clause automatically.
    CTEQuery("f").As(
        Select("id", "a").From("foo"),
    ),

    // CTETable will be added to FROM clause automatically.
    // We can read CTETable as "CTE query to be used as a table".
    CTETable("b").As(
        Select("b").From("bar"),
    ),
).Select("t.id", "f.a", "b.b").From("t").Join(
    "f", "f.id = t.id",
)
println(b.String())

// Output:
// WITH f AS (SELECT id, a FROM foo), b AS (SELECT b FROM bar) SELECT t.id, f.a, b.b FROM t, b JOIN f ON f.id = t.id

My initial design, adding CTE table names to FROM clause automatically, is to improve readability of related Go code.

From the sample code below, we can see that using this design makes the writing of Go code very similar to the writing of SQL itself, which should enhance the readability and maintainability of the code.

func ExampleWith() {
sb := With(
CTETable("users", "id", "name").As(
Select("id", "name").From("users").Where("name IS NOT NULL"),
),
CTETable("devices").As(
Select("device_id").From("devices"),
),
).Select("users.id", "orders.id", "devices.device_id").Join(
"orders",
"users.id = orders.user_id",
"devices.device_id = orders.device_id",
)
fmt.Println(sb)
// Output:
// WITH users (id, name) AS (SELECT id, name FROM users WHERE name IS NOT NULL), devices AS (SELECT device_id FROM devices) SELECT users.id, orders.id, devices.device_id FROM users, devices JOIN orders ON users.id = orders.user_id AND devices.device_id = orders.device_id
}

Solution 2: BREAKING CHANGE - Explicitly set table names in SelectBuilder.With() method to add CTE table in FROM clause

Another way to solve this issue is to completely remove the side effect, with which all CTE table names will be automatically added to FROM clause. Instead, adding second param names in SelectBuilder.With(builder *CTEBuilder, names ...string) to allow us to add CTE table names to FROM clause explicitly.

Here is a sample.

cteb := With(
    CTETable("f").As(
        Select("id", "a").From("foo"),
    ),
    CTETable("b").As(
        Select("b").From("bar"),
    ),
)
sb := Select("t.id", "f.a", "b.b").From("t").Join(
    "f", "f.id = t.id",
).With(cteb, "b") // Explicitly add `b` in `FROM` clause
println(sb.String())

// Output:
// WITH f AS (SELECT id, a FROM foo), b AS (SELECT b FROM bar) SELECT t.id, f.a, b.b FROM t, b JOIN f ON f.id = t.id

There could be other solutions to solve this issue. Looking forward to any feedback on this topic.

cc @slok @iambudi, because of your previous contribution on CTE.

Repository owner deleted a comment Aug 26, 2024
Repository owner deleted a comment Aug 26, 2024
Repository owner deleted a comment Aug 26, 2024
Repository owner deleted a comment Aug 26, 2024
@arikkfir
Copy link
Contributor

arikkfir commented Sep 4, 2024

👍 for solution 1 from me too

@iambudi
Copy link

iambudi commented Sep 4, 2024

I choose solution #1, no breaking changes and "makes the writing of Go code very similar to the writing of SQL itself".

@huandu
Copy link
Owner Author

huandu commented Sep 5, 2024

Thanks all for your feedback. I'll implement the solution 1 by this weekend.

@huandu huandu closed this as completed in 01acaab Sep 8, 2024
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

4 participants
@huandu @arikkfir @iambudi and others