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

Insert values except / merge #183

Open
CrownedPhoenix opened this issue Jun 13, 2024 · 3 comments
Open

Insert values except / merge #183

CrownedPhoenix opened this issue Jun 13, 2024 · 3 comments
Labels
enhancement New feature or request

Comments

@CrownedPhoenix
Copy link

I'd like do be able to structure a query like:

INSERT INTO <table>
VALUES <values>  EXCEPT
SELECT <cols> FROM <table>

There might be a way to do this that I'm not sure of but some of the directions I went down were:

db.insert(into: <table>).values(...).except(...) // Not an API

db.insert(into: <table>).select(SQLSubQuery.select({
  $0.values(...).union(...) // Not an API
}))

db.insert(into: <table>).select(SQLSubQuery.except({
    $0.values(...)
  }).except(...) // Not an API
)

Context: My ultimate objective is to perform a merge but I know that is not general SQL so I'm trying to get as close an approximation I can with a single query.

@CrownedPhoenix CrownedPhoenix added the enhancement New feature or request label Jun 13, 2024
@gwynne
Copy link
Member

gwynne commented Jun 13, 2024

The union builder API is a little weird when it comes to subqueries, and I forgot to add proper support for inserts. There also isn't a query expression for Postgres's VALUES command yet, and unions only support SELECT queries. You can try this as a rather verbose (and slightly bizarre) workaround:

/// Implements a VALUES() query expression.
struct PostgreSQLValuesQuery: SQLExpression {
    var columns: [any SQLExpression] = []
    var orderBys: [any SQLExpression] = []
    var limit: Int?
    var offset: Int?

    func serialize(to serializer: inout SQLSerializer) {
        serializer.statement {
            $0.append("VALUES", SQLList(self.columns))
            if !self.orderBys.isEmpty {
                $0.append("ORDER BY", SQLList(self.orderBys))
            }
            if let limit = self.limit {
                $0.append("LIMIT", SQLLiteral.numeric("\(limit)"))
            }
            if let offset = self.offset {
                $0.append("OFFSET", SQLLiteral.numeric("\(offset)"))
            }
        }
    }
}

/// Provides a query builder for VALUES() queries
final class PostgreSQLValuesBuilder: SQLQueryBuilder, SQLUnqualifiedColumnListBuilder, SQLPartialResultBuilder {
    var database: any SQLDatabase
    var valuesQuery: PostgreSQLValuesQuery
    var query: any SQLExpression { self.valuesQuery }

    var columnList: [any SQLExpression] {
        get { self.valuesQuery.columns }
        set { self.valuesQuery.columns = newValue }
    }
    
    var orderBys: [any SQLExpression] {
        get { self.valuesQuery.orderBys }
        set { self.valuesQuery.orderBys = newValue }
    }

    var limit: Int? {
        get { self.valuesQuery.limit }
        set { self.valuesQuery.limit = newValue }
    }
    
    var offset: Int? {
        get { self.valuesQuery.offset }
        set { self.valuesQuery.offset = newValue }
    }
}

extension SQLDatabase {
    /// Provides a builder for a top-level VALUES() query
    func values() -> PostgreSQLValuesBuilder {
        .init(database: self, valuesQuery: .init())
    }
}

/// Provides a query builder for VALUES() subqueries
final class PostgreSQLValuesSubqueryBuilder: SQLUnqualifiedColumnListBuilder, SQLPartialResultBuilder {
    var valuesQuery: PostgreSQLValuesQuery

    var columnList: [any SQLExpression] {
        get { self.valuesQuery.columns }
        set { self.valuesQuery.columns = newValue }
    }
    
    var orderBys: [any SQLExpression] {
        get { self.valuesQuery.orderBys }
        set { self.valuesQuery.orderBys = newValue }
    }

    var limit: Int? {
        get { self.valuesQuery.limit }
        set { self.valuesQuery.limit = newValue }
    }
    
    var offset: Int? {
        get { self.valuesQuery.offset }
        set { self.valuesQuery.offset = newValue }
    }
}

extension SQLSubquery {
    /// Provides a builder for a VALUES() subuqery
    static func values(
        _ build: (PostgreSQLValuesSubqueryBuilder) throws -> PostgreSQLValuesSubqueryBuilder
    ) rethrows -> some SQLExpression {
        let builder = PostgreSQLValuesSubqueryBuilder(valuesQuery: .init())

        _ = try build(builder)
        return builder.valuesQuery
    }
}

/// Finally, you can now do this:
let builder = db.insert(into: <table>)
builder.insert.valueQuery = SQLList([
    SQLSubquery.values { $0
        .column(/* one of your VALUES() expressions goes here */)
        .column(/* another VALUES() expression can go here */)
        // etc.
    },
    SQLUnionJoiner(type: .except),
    SQLSubquery.select { $0
        // you can do all the usual SELECT query builder stuff here
    }
], separator: SQLRaw(" "))
try await builder.run()

It's a pretty hacky workaround and requires a bunch of support code, I know 😕 It also assumes Postgres's VALUES() syntax, which is quite different from MySQL's version, hence the naming of the types. Last but not least, this has all been typed in a Github comment field, I have not even checked if it compiles, so apologies for any typos or bugs 😰.

@gwynne
Copy link
Member

gwynne commented Jun 13, 2024

Or, if you'd rather not do all that, you can always punt to a raw query string:

try await db.raw("""
    INSERT INTO \(ident: "table")
        VALUES (...)
        EXCEPT
        SELECT <cols> FROM <table>
    """).run()

😅 I tend not to recommend this approach because it's much too easy to forget to escape things or use bound parameters with raw queries, but it's considerably less code than the other in this case, to say the least.

@CrownedPhoenix
Copy link
Author

I really appreciate this! I'm happy to use a workaround for now. I'd like to get more comfortable with extending the builder API as needed anyway so this is a helpful reference to follow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants