skelpo / FluentQuery

Swift lib that gives ability to build complex raw SQL-queries in a more easy way using KeyPaths. Built for Vapor and Fluent 3

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Mihael Isaev

MIT License Swift 4.1 Twitter


Quick Intro

struct PublicUser: Codable {
    var name: String
    var petName: String
    var petType: String
    var petToysQuantity: Int
}
try FluentQuery()
    .select(all: User.self)
    .select(\Pet.name, as: "petName")
    .select(\PetType.name, as: "petType")
    .select(.count(\PetToy.id), as: "petToysQuantity")
    .from(User.self)
    .join(.left, Pet.self, where: FQWhere(\Pet.id == \User.idPet))
    .join(.left, PetType.self, where: FQWhere(\PetType.id == \Pet.idType))
    .join(.left, PetToy.self, where: FQWhere(\PetToy.idPet == \Pet.id))
    .groupBy(FQGroupBy(\User.id).and(\Pet.id).and(\PetType.id).and(\PetToy.id))
    .execute(on: conn)
    .decode(PublicUser.self) // -> Future<[PublicUser]> πŸ”₯πŸ”₯πŸ”₯

Intro

It's a swift lib that gives ability to build complex raw SQL-queries in a more easy way using KeyPaths.

Built for Vapor3 and depends on Fluent package because it uses Model.reflectProperty(forKey:) method to decode KeyPaths.

For now I developing it for Postgres queries so it is with Postgres's SQL-syntax only.

Now it supports: query with most common predicates, building json objects in select, subqueries, subquery into json, joins, aggregate functions, etc.

Note: the project is in active development state and it may cause huge syntax changes before v1.0.0

If you have great ideas of how to improve this package write me (@iMike) in Vapor's discord chat or just send pull request.

Hope it'll be useful for someone :)

Install through Swift Package Manager

Edit your Package.swift

//add this repo to dependencies
.package(url: "https://github.com/MihaelIsaev/FluentQuery.git", from: "0.4.2")
//and don't forget about targets
//"FluentQuery"

One more little intro

I love to write raw SQL queries because it gives ability to flexibly use all the power of database engine.

And Vapor's Fleunt allows you to do raw queries, but the biggest problem of raw queries is its hard to maintain them.

I faced with that problem and I started developing this lib to write raw SQL queries in swift-way by using KeyPaths.

And let's take a look what we have :)

How it works

First of all you need to import the lib

import FluentQuery

Then create FluentQuery object to do some building and get raw query string

let query = FluentQuery()
//some building
let rawQuery: String = query.build()

Let's take a look how to use it with some example request

Imagine that you have a list of cars

So you have Car fluent model

final class Car: Model {
  var id: UUID?
  var year: String
  var color: String
  var engineCapacity: Double
  var idBrand: UUID
  var idModel: UUID
  var idBodyType: UUID
  var idEngineType: UUID
  var idGearboxType: UUID
}

and related models

final class Brand: Decodable {
  var id: UUID?
  var value: String
}
final class Model: Decodable {
  var id: UUID?
  var value: String
}
final class BodyType: Decodable {
  var id: UUID?
  var value: String
}
final class EngineType: Decodable {
  var id: UUID?
  var value: String
}
final class GearboxType: Decodable {
  var id: UUID?
  var value: String
}

ok, and you want to get every car as convenient codable model

struct PublicCar: Content {
  var id: UUID
  var year: String
  var color: String
  var engineCapacity: Double
  var brand: Brand
  var model: Model
  var bodyType: BodyType
  var engineType: EngineType
  var gearboxType: GearboxType
}

Here's example request code for that situation

func getListOfCars(_ req: Request) throws -> Future<[PublicCar]> {
  return req.requestPooledConnection(to: .psql).flatMap { conn -> EventLoopFuture<[PublicCar]> in
      defer { try? req.releasePooledConnection(conn, to: .psql) }
      return FluentQuery()
        .select(distinct: \Car.id)
        .select(\Car.year, as: "year")
        .select(\Car.color, as: "color")
        .select(\Car.engineCapacity, as: "engineCapacity")
        .select(.row(Brand.self), as: "brand")
        .select(.row(Model.self), as: "model")
        .select(.row(BodyType.self), as: "bodyType")
        .select(.row(EngineType.self), as: "engineType")
        .select(.row(GearboxType.self), as: "gearboxType")
        .from(Car.self)
        .join(.left, Brand.self, where: FQWhere(\Brand.id == \Car.idBrand))
        .join(.left, Model.self, where: FQWhere(\Model.id == \Car.idModel))
        .join(.left, BodyType.self, where: FQWhere(\BodyType.id == \Car.idBodyType))
        .join(.left, EngineType.self, where: FQWhere(\EngineType.id == \Car.idEngineType))
        .join(.left, GearboxType.self, where: FQWhere(\GearboxType.id == \Car.idGearboxType))
        .groupBy(FQGroupBy(\Car.id)
          .and(\Brand.id)
          .and(\Model.id)
          .and(\BodyType.id)
          .and(\EngineType.id)
          .and(\GearboxType.id)
        )
        .orderBy(FQOrderBy(\Brand.value, .ascending)
          .and(\Model.value, .ascending)
        )
        .execute(on: conn)
        .decode(PublicCar.self)
  }
}

Hahah, that's cool right? πŸ˜ƒ

As you can see we've build complex query to get all depended values and decoded postgres raw response to our codable model.

BTW, this is a raw SQL equivalent
SELECT
DISTINCT c.id,
c.year,
c.color,
c."engineCapacity",
(SELECT toJsonb(brand)) as "brand",
(SELECT toJsonb(model)) as "model",
(SELECT toJsonb(bt)) as "bodyType",
(SELECT toJsonb(et)) as "engineType",
(SELECT toJsonb(gt)) as "gearboxType"
FROM "Cars" as c
LEFT JOIN "Brands" as brand ON c."idBrand" = brand.id
LEFT JOIN "Models" as model ON c."idModel" = model.id
LEFT JOIN "BodyTypes" as bt ON c."idBodyType" = bt.id
LEFT JOIN "EngineTypes" as et ON c."idEngineType" = et.id
LEFT JOIN "GearboxTypes" as gt ON c."idGearboxType" = gt.id
GROUP BY c.id, brand.id, model.id, bt.id, et.id, gt.id
ORDER BY brand.value ASC, model.value ASC

So why do you need to use this lib for your complex queries?

The reason #1 is KeyPaths!

If you will change your models in the future you'll have to remember where you used links to this model properties and rewrite them manually and if you forgot one you will get headache in production. But with KeyPaths you will be able to compile your project only while all links to the models properties are up to date. Even better, you will be able to use refactor functionality of Xcode! πŸ˜„

The reason #2 is if/else statements

With FluentQuery's query builder you can use if/else wherever you need. And it's super convenient to compare with using if/else while createing raw query string. πŸ˜‰

The reason #3

It is faster than multiple consecutive requests

The reason #4

You can join on join on join on join on join on join 😁😁😁

With this lib you can do real complex queries! πŸ”₯ And you still flexible cause you can use if/else statements while building and even create two separate queries with the same basement using let separateQuery = FluentQuery(copy: originalQuery) πŸ•Ί

Methods

The list of the methods which FluentQuery provide with

Select

These methods will add fields which will be used between SELECT and FROM

SELECT _here_some_fields_list_ FROM

So to add what you want to select call these methods one by one

Method SQL equivalent
.select("*") *
.select(all: Car.self) "Cars".*
.select(all: someAlias) "some_alias".*
.select(\Car.id) "Car".id
.select(someAlias.k(.id)) "some_alias".id
.select(distinct: \Car.id) DISTINCT "Car".id
.select(distinct: someAlias.k(.id)) DISTINCT "some_alias".id
.select(.count(\Car.id), as: "count") COUNT("Cars".id) as "count"
.select(.sum(\Car.value), as: "sum") SUM("Cars".value) as "sum"
.select(.average(\Car.value), as: "average") AVG("Cars".value) as "average"
.select(.min(\Car.value), as: "min") MIN("Cars".value) as "min"
.select(.max(\Car.value), as: "max") MAX("Cars".value) as "max"

BTW, read about aliases below

From

Method SQL equivalent
.from("Table") FROM "Table"
.from(raw: "Table") FROM Table
.from(Car.self) FROM "Cars" as "cars"
.from(someAlias) FROM "SomeAlias" as "someAlias"

Join

.join(FQJoinMode, Table, where: FQWhere)

enum FQJoinMode {
    case left, right, inner, outer
}

As Table you can put Car.self or someAlias

About FQWhere please read below

Where

.where(FQWhere)

FQWhere(predicate).and(predicate).or(predicate).and(FQWhere).or(FQWhere)

What predicate is?

It may be KeyPath operator KeyPath or KeyPath operator Value

KeyPath may be \Car.id or someAlias.k(\.id)

Value may be any value like int, string, uuid, array, or even something optional or nil

List of available operators you saw above in cheatsheet

Some examples

FQWhere(someAlias.k(\.deletedAt) == nil)
FQWhere(someAlias.k(\.id) == 12).and(\Car.color ~~ ["blue", "red", "white"])
FQWhere(\Car.year == "2018").and(\Brand.value !~ ["Chevrolet", "Toyota"])
FQWhere(\Car.year != "2005").and(someAlias.k(\.engineCapacity) > 1.6)
Where grouping example

if you need to group predicates like

"Cars"."engineCapacity" > 1.6 AND ("Brands".value LIKE '%YO%' OR "Brands".value LIKE '%ET')

then do it like this

FQWhere(\Car.engineCapacity > 1.6).and(FQWhere(\Brand.value ~~ "YO").or(\Brand.value ~= "ET"))
Cheatsheet
Operator SQL equivalent
== == / IS
!= != / IS NOT
> >
< <
>= >=
<= <=
~~ IN ()
!~ NOT IN ()
~= LIKE '%str'
~~ LIKE '%str%'
=~ LIKE 'str%'
~% ILIKE '%str'
%% ILIKE '%str%'
%~ ILIKE 'str%'
!~= NOT LIKE '%str'
!~~ NOT LIKE '%str%'
!=~ NOT LIKE 'str%'
!~% NOT ILIKE '%str'
!%% NOT ILIKE '%str%'
!%~ NOT ILIKE 'str%'

Having

.having(FQWhere)

About FQWhere you already read above, but as having calls after data aggregation you may additionally filter your results using aggreagate functions such as SUM, COUNT, AVG, MIN, MAX

.having(FQWhere(.count(\Car.id) > 0))
//OR
.having(FQWhere(.count(someAlias.k(\.id)) > 0))
//and of course you an use .and().or().groupStart().groupEnd()

Group by

.groupBy(FQGroupBy(\Car.id).and(\Brand.id).and(\Model.id))

Order by

.orderBy(FQOrderBy(\Car.year, .ascending).and(someAlias.k(\.name), .descending))

Offset

Method SQL equivalent
.offset(0) OFFSET 0

Limit

Method SQL equivalent
.limit(30) LIMIT 30

JSON

You can build json on jsonb object by creating FQJSON instance

Instance SQL equivalent
FQJSON(.normal) build_json_object()
FQJSON(.binary) build_jsonb_object()

After creating instance you should fill it by calling .field(key, value) method like

FQJSON(.binary).field("brand", \Brand.value).field("model", someAlias.k(\.value))

as you may see it accepts keyPaths and aliased keypaths

but also it accept function as value, here's the list of available functions

Function SQL equivalent
row(Car.self) SELECT row_to_json("Cars")
row(someAlias) SELECT row_to_json("some_alias")
extractEpochFromTime(\Car.createdAt) extract(epoch from "Cars"."createdAt")
extractEpochFromTime(someAlias.k(.createdAt)) extract(epoch from "some_alias"."createdAt")
count(\Car.id) COUNT("Cars".id)
count(someAlias.k(.id)) COUNT("some_alias".id)
countWhere(\Car.id, FQWhere(\Car.year == "2012")) COUNT("Cars".id) filter (where "Cars".year == '2012')
countWhere(someAlias.k(.id), FQWhere(someAlias.k(.id) > 12)) COUNT("some_alias".id) filter (where "some_alias".id > 12)

Aliases

FQAlias<OriginalClass>(aliasKey)

When you write complex query you can several joins or subqueries to the same table and you need to use aliases for that like "Cars" as c

So with FluentQuery you can create aliases like this

let aliasBrand = FQAlias<Brand>("b")
let aliasModel = FQAlias<Model>("m")
let aliasEngineType = FQAlias<EngineType>("e")

and you can use KeyPaths of original tables referenced to these aliases like this

aliasBrand.k(\.id)
aliasBrand.k(\.value)
aliasModel.k(\.id)
aliasModel.k(\.value)
aliasEngineType.k(\.id)
aliasEngineType.k(\.value)

Executing query

.execute(on: PostgreSQLConnection)

try FluentQuery().select(all: User.self).execute(on: conn)

Decoding query

.decode(Decodable.Type, dateDecodingstrategy: JSONDecoder.DateDecodingStrategy?)

try FluentQuery().select(all: User.self).execute(on: conn).decode(PublicUser.self)

by default date decoding strategy is yyyy-MM-dd'T'HH:mm:ss.SSS'Z' which is compatible with postgres timestamp

but you can specify custom DateDecodingStrategy like this

try FluentQuery().select(all: User.self).execute(on: conn).decode(PublicUser.self, dateDecodingStrategy: .secondsSince1970)

or like this

let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
try FluentQuery().select(all: User.self).execute(on: conn).decode(PublicUser.self, dateDecodingStrategy: .formatted(formatter))

Conslusion

I hope that it'll be useful for someone.

Feedback is really appreciated!

And don't hesitate to asking me questions, I'm ready to help in Vapor's discord chat find me by @iMike nickname.

About

Swift lib that gives ability to build complex raw SQL-queries in a more easy way using KeyPaths. Built for Vapor and Fluent 3

License:MIT License


Languages

Language:Swift 100.0%