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

Add support for the WITH clause #1139

Merged
merged 8 commits into from
Jul 17, 2022
Merged

Conversation

marmphco
Copy link
Contributor

@marmphco marmphco commented Jul 8, 2022

Hello! I have been using SQLite.swift in one of my projects and so far it's been working great. Just one thing is missing that my project needs: the WITH clause. The WITH clause provides the ability to do hierarchical or recursive queries of tree and graph-like data. See https://www.sqlite.org/lang_with.html.

Thanks in advance for reviewing 🙂

API changes

  • Add an all parameter to QueryType.union to allow UNION ALL to be used in a query. UNION ALL allows duplicate rows in the result, which may help improve performance for large recursive queries. I opted to add the parameter to the start of the list so that it does not dangle at the end when the union's query is long:

    users.union(all: true, posts.join(users, on: users[id] == posts[userId]))
    // I think it's a little easier to read than:
    users.union(posts.join(users, on: users[id] == posts[userId]), all: true)
  • Add with function to QueryType. This function adds a WITH clause to a query. The function may be called multiple times to add multiple clauses to a query. If multiple clauses are added to the query with conflicting recursive parameters, the whole WITH clause will be considered recursive.

    Like the union function, I put the query parameter at the end so that the recursive and materializationHint options don't dangle at the end of a long query.

    let users = Table("users")
    let users = Table("posts")
    let first = Table("first")
    let second = Table("second")
    first.with(first, recursive: true as: users).with(second, recursive: false, as: posts)
    // WITH RECURSIVE "first" AS (SELECT * from users), "second" AS (SELECT * from posts) SELECT * from "first"

Other

I left one linter warning unfixed: there's a file length violation in Query.swift. The file is 519 lines long, 19 over the limit. I'm not sure how to bring the line length back down below 500 without harming readability or moving a lot of stuff around (maybe you do?). Any suggestions on what to do here?

The `WITH` clause provides the ability to do hierarchical or recursive queries of tree and graph-like data. See https://www.sqlite.org/lang_with.html.

**Details**

- Add `all` parameter to `QueryType.union` to allow `UNION ALL` to be used in a query. I opted to add the parameter to the start of the list so that it does not dangle at the end when the union's query is long:

```swift
users.union(all: true, posts.join(users, on: users[id] == posts[userId]))
// It's a little easier to read than:
users.union(posts.join(users, on: users[id] == posts[userId]), all: true)
```

- Add `with` function to `QueryType`. This function adds a `WITH` clause to a query. The function may be called multiple times to add multiple clauses to a query. If multiple clauses are added to the query with conflicting `recursive` parameters, the whole `WITH` clause will be considered recursive.

  Like the `union` function, I put the `subquery` parameter at the end so that the `recursive` and `materializationHint` options don't dangle at the end of a long query.

```swift
let users = Table("users")
let users = Table("posts")
let first = Table("first")
let second = Table("second")
first.with(first, recursive: true as: users).with(second, recursive: false, as: posts)
// WITH RECURSIVE "first" AS (SELECT * from users), "second" AS (SELECT * from posts) SELECT * from "first"
```
Note sure how to get the line length of Query back down below 500 without harming readability though.
@marmphco marmphco changed the title Add support for WITH clause Add support for the WITH clause Jul 8, 2022
@jberkel
Copy link
Collaborator

jberkel commented Jul 12, 2022

@marmphco thanks for the PR. the simplest solution for the linter problem would be to start splitting Query into functional extensions, for example Query+with.swift.

@marmphco
Copy link
Contributor Author

@jberkel Good point, I'll try that out.

// MARK: - Private

struct WithClauses {
Copy link
Collaborator

Choose a reason for hiding this comment

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

these could be moved to the new file as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it, will move. Wasn't sure whether to move them since they kind of feel like part of QueryClauses.

Copy link
Collaborator

Choose a reason for hiding this comment

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

MaterializationHint probably as well since it's only used there

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🤦 of course, my bad!

@@ -59,6 +59,11 @@ class QueryTests: XCTestCase {
assertSQL("SELECT DISTINCT * FROM \"users\"", users.select(distinct: *))
}

func test_union_compilesUnionClause() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

split:

test_union_compiles_Union_Clause
test_union_compiles_Union_All_Clause

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I split the tests in cfa6010. I kept the names in the form test_union_compilesUnionAllClause for consistency with the existing tests. Let me know if I should instead do test_union_compiles_Union_All_Clause.

temp.with(temp, as: users))
}

func test_with_recursive_compilesWithClause() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

compiles_with_clause
compiles_with_recursive_clause

temp.with(temp, recursive: false, as: users))
}

func test_with_materialization_compilesWithClause() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

with_materialized
with_not_materialized

/// - table: A query representing the other table.
///
/// - Returns: A query with the given `UNION` clause applied.
public func union(_ table: QueryType) -> Self {
public func union(all: Bool = false, _ table: QueryType) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree that putting all in front of the parameter list is clearer here (and mimics the SQL), but as you say this will break API compatibility, so we'll have to release it as 0.14.

@jberkel jberkel merged commit 642d4fa into stephencelis:master Jul 17, 2022
@marmphco marmphco deleted the recursive-cte branch July 18, 2022 06:38
marmphco added a commit to marmphco/SQLite.swift that referenced this pull request Dec 23, 2022
In  stephencelis#1139 I introduced support for the `WITH` clause. My implementation contains a bug: the statement preparer doesn't produce the correct result column names for queries containing a `SELECT *` preceded by a `WITH`.

For example, consider the following statement:

```
WITH temp AS ( SELECT id, email from users) SELECT * from temp
```

An error would be thrown when preparing this statement because the glob expansion procedure would try to look up the column names for the result by looking up the column names for the query `SELECT * from temp`. This does not work because `temp` is a temporary view defined in the `WITH` clause.

To fix this, I modified the glob expansion procedure to include the `WITH` clause in the query used to look up the result column names.
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