mardi 26 janvier 2021

Compose SQL queries with constraints using natural logical expressions in Go

I like being fancy. Go doesn't really let you get too fancy. I recognize that it's quite intentional. Still...

I am writing a database interface library, and one of my goals is to use inferences about Go-like logical expressions to compose SQL, with some amount of runtime validation and some basic compile-time validation. There are already libraries that provide an API that looks familiar to a SQL pro, and there are a few thin wrappers like SQLX that just "grease the wheels" as it were. What there is not, to my knowledge, is a library with an API that allows the consumer to use Go-like expressions to compose queries.

This is targeted for an ecosystem where Go is the server language. I do some personal stuff in Rust, and macros would render this an absolute non-issue. I could write this so fast in Rust. Go is a different thing entirely.

The Go AST package looks like it provides a similar kind of tokenized, analyzable representation of code as does the proc_macro::TokenStream crate, which would allow the below "desired API" to be completely achievable. The problem is that I'm writing a library, and cannot depend on knowing the path of the file that's importing it. Even operator overloading, a very common language feature, is missing from Go (intentionally, I know, but on an ill-conceived rationale).

In the below example, I'm using reflection to guess what fields would exist on the in[] types used for the anonymous functions, and using a pseudo-operator representation of logical operations that compares pointer references to know what fields are being used in such operations, and whether Go values known in advance are being used for comparison.

The "Truth" interface returned by the pseudo-operator functions is evaluated into SQL logical expression, with the pointer addresses resolved into a string fieldname from the struct type passed in the argument by comparing pointer addresses between fields of a dummied zero-value struct of passed type and the arguments given to the pseudo-operator, unless the argument is another Truth interface, in which case recursion.

If I were writing in Rust, I just provide a transliteration macro, parse the TokenStream, and create equivalent SQL. But It's a library for a Go project, not Rust...

Note that these examples rely on extremely heavy use of reflection to do exactly the kinds of things Go was not meant to do.

Again, remember that the below composes two SQL statements, and reduces them into an array of objects, each having an array of objects.

    person, _ := source.Get("person") //fetch table metadata
    quirk, _ := source.Get("quirks") //fetch other table metadata
    //pretend I'm actually error checking them

    var personArray []Person

    _ := person.Filter(
        func(personRow Person) Truth { //defined type that represents a row in person table. Reflect...
            return Equality(&personRow.TookAssessment, literal(1))
        },
        //error handle-ey function, ala Javascript Promise
    ).Attach(
        quirk,
        func(personRow Person, quirkRow Quirk) Truth {
            return All(
                Equality(&quirkRow.IsActive,Literal(1)),
                Equality(&quirkRow.PersonId, &personRow.PersonId),
            )
        },
    ).Execute().Collect(&personArray)
    //pretend I'm collecting a possible error value returned and properly handling it.

That's what I'm working towards implementing it. I need not tell you that the above is cumbersome and fragile. It's not entirely clear that it only works if the consumer supplies references. It's like an exercise in "principle of MOST astonishment."

I want a way to use native Go logic in the expression, but I can't see a way to do any better than I demonstrate above.





Aucun commentaire:

Enregistrer un commentaire