It’s a swift lib that gives ability to build complex raw SQL-queries in a more easy way using KeyPaths. I call it FQL 😎
Built for Vapor3 and depends on Fluent package because it uses Model.reflectProperty(forKey:) method to decode KeyPaths.
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.30")
//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 FQL object, build your SQL query using methods described below and as first step just print it as a raw string
let query = FQL()
//some building
print("rawQuery: \(query)")
Several examples
1. Simple
// SELECT * FROM "User" WHERE age > 18
let fql = FQL().select(all: User.self)
.from(User.self)
.where(\User.age > 18)
.execute(on: conn)
.decode(User.self)
2. Simple with join
// SELECT u.*, r.name as region FROM "User" as u WHERE u.age > 18 LEFT JOIN "UserRegion" as r ON u.idRegion = r.id
let fql = FQL().select(all: User.self)
.select(\UserRegion.name)
.from(User.self)
.where(\User.age > 18)
.join(.left, UserRegion.self, where: \User.idRegion == \UserRegion.id)
.execute(on: conn)
.decode(UserWithRegion.self)
3. Medium 🙂 with query into jsonB obejcts
// SELECT (SELECT to_jsonb(u)) as user, (SELECT to_jsonb(r)) as region FROM "User" as u WHERE u.age > 18 LEFT JOIN "UserRegion" as r ON u.idRegion = r.id
let fql = FQL().select(.row(User.self), as: "user")
.select(.row(UserRegion.self), as: "region")
.from(User.self)
.where(\User.age > 18)
.join(.left, UserRegion.self, where: \User.idRegion == \UserRegion.id)
.execute(on: conn)
.decode(UserWithRegion.self)
// in this case UserWithRegion struct will look like this
struct UserWithRegion: Codable {
var user: User
var region: UserRegion
}
4. Complex
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
}
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 FQL‘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 = FQL(copy: originalQuery) 🕺
Methods
The list of the methods which FQL 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
Case sensitive text search where text not like string
!~~
NOT LIKE ‘%str%’
!=~
NOT LIKE ‘str%’
!~%
NOT ILIKE ‘%str’
Case insensitive text search where text not like string
!%%
NOT ILIKE ‘%str%’
!%~
NOT ILIKE ‘str%’
~~~
@@ ‘str’
Full text search
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()
FQAlias<OriginalClass>(aliasKey) or OriginalClass.alias(aliasKey)
Also you can use static alias OriginalClass.alias if you need only one its variation
And you can generate random alias OriginalClass.randomAlias but keep in mind that every call to randomAlias generates new alias as it’s computed property
What’s that for?
When you write complex query you may have several joins or subqueries to the same table and you need to use aliases for that like "Cars" as c
Usage
So with FQL you can create aliases like this
//"CarBrand" as b
let aliasBrand = CarBrand.alias("b")
//"CarModel" as m
let aliasModel = CarModel.alias("m")
//"EngineType" as e
let aliasEngineType = EngineType.alias("e")
and you can use KeyPaths of original tables referenced to these aliases like this
or if you have two or more columns with different date format in the same model then you could create your own date formatter like described in issue #3
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.
⚠️ This lib is DEPRECATED ⚠️ please use SwifQL with Bridges
Quick Intro
Intro
It’s a swift lib that gives ability to build complex raw SQL-queries in a more easy way using KeyPaths. I call it FQL 😎
Built for Vapor3 and depends on
Fluent
package because it usesModel.reflectProperty(forKey:)
method to decode KeyPaths.Install through Swift Package Manager
Edit your
Package.swift
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
Then create
FQL
object, build your SQL query using methods described below and as first step just print it as a raw stringSeveral examples
1. Simple
2. Simple with join
3. Medium 🙂 with query into jsonB obejcts
4. Complex
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 modeland related models
ok, and you want to get every car as convenient codable model
Here’s example request code for that situation
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
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
statementsWith
FQL
‘s query builder you can useif/else
wherever you need. And it’s super convenient to compare with usingif/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 = FQL(copy: originalQuery)
🕺Methods
The list of the methods which
FQL
provide withSelect
These methods will add fields which will be used between
SELECT
andFROM
SELECT _here_some_fields_list_ FROM
So to add what you want to select call these methods one by one
FQOver
, as: “rowNumber”)EXPRESSION
ORDER BYSOMETHING
) as “rowNumber”BTW, read about aliases and
FQOver
belowWindow functions
If you need to use window functions like rowNumber, rank, dense_rank, etc. like this
(refer to: https://www.postgresql.org/docs/current/static/functions-window.html)
then you could build it like this
and then use it in your query like this
From
Join
.join(FQJoinMode, Table, where: FQWhere)
As
Table
you can putCar.self
orsomeAlias
About
FQWhere
please read belowWhere
.where(FQWhere)
You can write where predicate two ways
First is object oriented
Second is predicate oriented
Example for AND statements
Example for OR statements
Example for both AND and OR statements
What FQWhere() doing here? It groups OR statements into round brackets to achieve
a AND (b OR c)
sql code.What
predicate
is?It may be
KeyPath operator KeyPath
orKeyPath operator Value
KeyPath
may be\Car.id
orsomeAlias.k(\.id)
Value
may be any value like int, string, uuid, array, or even something optional or nilList of available operators you saw above in cheatsheet
Some examples
Where grouping example
if you need to group predicates like
then do it like this
Cheatsheet
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 asSUM, COUNT, AVG, MIN, MAX
Group by
or
or
Order by
or
Offset
Limit
JSON
You can build
json
onjsonb
object by creatingFQJSON
instanceAfter creating instance you should fill it by calling
.field(key, value)
method likeas you may see it accepts keyPaths and aliased keypaths
but also it accept function as value, here’s the list of available functions
Aliases
FQAlias<OriginalClass>(aliasKey)
orOriginalClass.alias(aliasKey)
Also you can use static alias
OriginalClass.alias
if you need only one its variationAnd you can generate random alias
OriginalClass.randomAlias
but keep in mind that every call torandomAlias
generates new alias as it’s computed propertyWhat’s that for?
When you write complex query you may have several joins or subqueries to the same table and you need to use aliases for that like
"Cars" as c
Usage
So with FQL you can create aliases like this
and you can use KeyPaths of original tables referenced to these aliases like this
Executing query
.execute(on: PostgreSQLConnection)
Decoding query
.decode(Decodable.Type, dateDecodingstrategy: JSONDecoder.DateDecodingStrategy?)
Custom DateDecodingStrategy
By default date decoding strategy is
yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
which is compatible with postgrestimestamp
But you can specify custom DateDecodingStrategy like this
or like this
or if you have two or more columns with different date format in the same model then you could create your own date formatter like described in issue #3
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.