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

sql protocols 2.0 #521

Merged
merged 9 commits into from Jun 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion Package.swift
Expand Up @@ -5,6 +5,7 @@ let package = Package(
name: "Fluent",
products: [
.library(name: "Fluent", targets: ["Fluent"]),
.library(name: "FluentSQL", targets: ["FluentSQL"]),
.library(name: "FluentBenchmark", targets: ["FluentBenchmark"]),
],
dependencies: [
Expand All @@ -15,13 +16,17 @@ let package = Package(
.package(url: "https://github.com/vapor/console.git", from: "3.0.0"),

// 🗄 Core services for creating database integrations.
.package(url: "https://github.com/vapor/database-kit.git", from: "1.0.0"),
.package(url: "https://github.com/vapor/database-kit.git", from: "1.2.0"),

// 📦 Dependency injection / inversion of control framework.
.package(url: "https://github.com/vapor/service.git", from: "1.0.0"),

// *️⃣ Build SQL queries in Swift. Extensible, protocol-based design that supports DQL, DML, and DDL.
.package(url: "https://github.com/vapor/sql.git", from: "2.0.0-beta"),
],
targets: [
.target(name: "Fluent", dependencies: ["Async", "Console", "Command", "Core", "DatabaseKit", "Logging", "Service"]),
.target(name: "FluentSQL", dependencies: ["Fluent", "SQL"]),
.testTarget(name: "FluentTests", dependencies: ["FluentBenchmark"]),
.target(name: "FluentBenchmark", dependencies: ["Fluent"]),
]
Expand Down
2 changes: 1 addition & 1 deletion Sources/Fluent/QueryBuilder/QueryBuilder+Range.swift
Expand Up @@ -7,7 +7,7 @@ extension QueryBuilder {
///
/// - returns: Query builder for chaining.
public func range(_ range: Range<Int>) -> Self {
return self.range(lower: range.lowerBound, upper: range.upperBound)
return self.range(lower: range.lowerBound, upper: range.upperBound - 1)
}

/// Limits the results of this query to the specified range.
Expand Down
1 change: 0 additions & 1 deletion Sources/FluentBenchmark/Benchmarks/BenchmarkContains.swift

This file was deleted.

39 changes: 39 additions & 0 deletions Sources/FluentBenchmark/Benchmarks/BenchmarkRange.swift
@@ -0,0 +1,39 @@
extension Benchmarker where Database: QuerySupporting {
fileprivate func _benchmark(on conn: Database.Connection) throws {
_ = try test(Galaxy<Database>(name: "Milky Way").save(on: conn))
_ = try test(Galaxy<Database>(name: "Andromeda").save(on: conn))
_ = try test(Galaxy<Database>(name: "Messier 82").save(on: conn))
_ = try test(Galaxy<Database>(name: "Tiangulum").save(on: conn))
_ = try test(Galaxy<Database>(name: "Sunflower").save(on: conn))

func testCount(_ builder: QueryBuilder<Database, Galaxy<Database>>, _ expected: Int, line: UInt = #line) throws {
let count = try test(builder.all(), line: line).count
if count != expected { fail("invalid count: \(count) != \(expected)", line: line) }
}

try testCount(Galaxy<Database>.query(on: conn).range(..<1), 1)
try testCount(Galaxy<Database>.query(on: conn).range(0..<1), 1)
try testCount(Galaxy<Database>.query(on: conn).range(...1), 2)
try testCount(Galaxy<Database>.query(on: conn).range(0...1), 2)
try testCount(Galaxy<Database>.query(on: conn).range(1...1), 1)
try testCount(Galaxy<Database>.query(on: conn).range(4..<5), 1)
try testCount(Galaxy<Database>.query(on: conn).range(3..<5), 2)
try testCount(Galaxy<Database>.query(on: conn).range(2..<4), 2)
}

public func benchmarkRange() throws {
let conn = try test(pool.requestConnection())
try self._benchmark(on: conn)
pool.releaseConnection(conn)
}
}

extension Benchmarker where Database: SchemaSupporting & MigrationSupporting {
public func benchmarkRange_withSchema() throws {
let conn = try test(pool.requestConnection())
defer { try? test(Galaxy<Database>.revert(on: conn)) }
try test(Galaxy<Database>.prepare(on: conn))
try self._benchmark(on: conn)
pool.releaseConnection(conn)
}
}
36 changes: 36 additions & 0 deletions Sources/FluentBenchmark/Benchmarks/BenchmarkSort.swift
@@ -0,0 +1,36 @@
extension Benchmarker where Database: QuerySupporting {
fileprivate func _benchmark(on conn: Database.Connection) throws {
_ = try test(Galaxy<Database>(name: "Milky Way").save(on: conn))
_ = try test(Galaxy<Database>(name: "Andromeda").save(on: conn))
_ = try test(Galaxy<Database>(name: "Messier 82").save(on: conn))
_ = try test(Galaxy<Database>(name: "Tiangulum").save(on: conn))
_ = try test(Galaxy<Database>(name: "Sunflower").save(on: conn))

func testCount(_ builder: QueryBuilder<Database, Galaxy<Database>>, _ expected: Int, line: UInt = #line) throws {
let count = try test(builder.all(), line: line).count
if count != expected { fail("invalid count: \(count) != \(expected)", line: line) }
}

let asc = try test(Galaxy<Database>.query(on: conn).sort(\.name, Database.querySortDirectionAscending).all())
let desc = try test(Galaxy<Database>.query(on: conn).sort(\.name, Database.querySortDirectionDescending).all())
if asc == desc {
fail("Did not sort.")
}
}

public func benchmarkSort() throws {
let conn = try test(pool.requestConnection())
try self._benchmark(on: conn)
pool.releaseConnection(conn)
}
}

extension Benchmarker where Database: SchemaSupporting & MigrationSupporting {
public func benchmarkSort_withSchema() throws {
let conn = try test(pool.requestConnection())
defer { try? test(Galaxy<Database>.revert(on: conn)) }
try test(Galaxy<Database>.prepare(on: conn))
try self._benchmark(on: conn)
pool.releaseConnection(conn)
}
}
35 changes: 35 additions & 0 deletions Sources/FluentBenchmark/Benchmarks/BenchmarkSubset.swift
@@ -0,0 +1,35 @@
extension Benchmarker where Database: QuerySupporting {
fileprivate func _benchmark(on conn: Database.Connection) throws {
let milkyWay = try test(Galaxy<Database>(name: "Milky Way").save(on: conn))
let andromeda = try test(Galaxy<Database>(name: "Andromeda").save(on: conn))
let messier82 = try test(Galaxy<Database>(name: "Messier 82").save(on: conn))
_ = try test(Galaxy<Database>(name: "Tiangulum").save(on: conn))
_ = try test(Galaxy<Database>(name: "Sunflower").save(on: conn))

func testCount(_ builder: QueryBuilder<Database, Galaxy<Database>>, _ expected: Int, line: UInt = #line) throws {
let count = try test(builder.all(), line: line).count
if count != expected { fail("invalid count: \(count) != \(expected)", line: line) }
}

try testCount(Galaxy<Database>.query(on: conn).filter(\.id ~~ []), 0)
try testCount(Galaxy<Database>.query(on: conn).filter(\.id ~~ [milkyWay.requireID()]), 1)
try testCount(Galaxy<Database>.query(on: conn).filter(\.id ~~ [milkyWay.requireID(), andromeda.requireID()]), 2)
try testCount(Galaxy<Database>.query(on: conn).filter(\.id ~~ [milkyWay.requireID(), andromeda.requireID(), messier82.requireID()]), 3)
}

public func benchmarkSubset() throws {
let conn = try test(pool.requestConnection())
try self._benchmark(on: conn)
pool.releaseConnection(conn)
}
}

extension Benchmarker where Database: SchemaSupporting & MigrationSupporting {
public func benchmarkSubset_withSchema() throws {
let conn = try test(pool.requestConnection())
defer { try? test(Galaxy<Database>.revert(on: conn)) }
try test(Galaxy<Database>.prepare(on: conn))
try self._benchmark(on: conn)
pool.releaseConnection(conn)
}
}
23 changes: 23 additions & 0 deletions Sources/FluentBenchmark/Benchmarks/Benchmarker.swift
Expand Up @@ -72,6 +72,29 @@ public final class Benchmarker<Database> where Database: LogSupporting {
}
}
}
extension Benchmarker where
Database: SchemaSupporting & MigrationSupporting & JoinSupporting & KeyedCacheSupporting & TransactionSupporting
{
public func runAll() throws {
try benchmarkAutoincrement_withSchema()
try benchmarkBugs_withSchema()
try benchmarkCache_withSchema()
try benchmarkChunking_withSchema()
try benchmarkIndexSupporting_withSchema()
try benchmarkJoins_withSchema()
try benchmarkLifecycle_withSchema()
try benchmarkModels_withSchema()
try benchmarkReferentialActions_withSchema()
try benchmarkRelations_withSchema()
try benchmarkSchema()
try benchmarkSoftDeletable_withSchema()
try benchmarkTimestampable_withSchema()
try benchmarkTransactions_withSchema()
try benchmarkRange_withSchema()
try benchmarkSubset_withSchema()
try benchmarkSort_withSchema()
}
}

final class BenchmarkLogger: DatabaseLogHandler {
/// Logs collected
Expand Down
12 changes: 12 additions & 0 deletions Sources/FluentBenchmark/Benchmarks/Galaxy.swift
@@ -0,0 +1,12 @@
struct Galaxy<Database>: Model, Equatable where Database: QuerySupporting {
typealias ID = UUID
static var idKey: IDKey { return \.id }
var id: UUID?
var name: String
init(id: UUID? = nil, name: String) {
self.name = name
}
}

extension Galaxy: AnyMigration, Migration where
Database: SchemaSupporting & MigrationSupporting { }
2 changes: 2 additions & 0 deletions Sources/FluentSQL/Exports.swift
@@ -0,0 +1,2 @@
@_exported import Fluent
@_exported import SQL
31 changes: 31 additions & 0 deletions Sources/FluentSQL/FluentSQLQuery.swift
@@ -0,0 +1,31 @@
public protocol FluentSQLQuery {
associatedtype Statement: FluentSQLQueryStatement
associatedtype Expression: SQLExpression
associatedtype Join: SQLJoin
associatedtype OrderBy: SQLOrderBy
associatedtype GroupBy: SQLGroupBy
associatedtype TableIdentifier: SQLTableIdentifier
associatedtype SelectExpression: SQLSelectExpression

var statement: Statement { get set }
var table: TableIdentifier { get set }
var keys: [SelectExpression] { get set }
var predicate: Expression? { get set }
var joins: [Join] { get set }
var orderBy: [OrderBy] { get set }
var groupBy: [GroupBy] { get set }
var limit: Int? { get set }
var offset: Int? { get set }
var values: [String: Expression] { get set }
var defaultBinaryOperator: Expression.BinaryOperator { get set }

static func query(_ statement: Statement, _ table: TableIdentifier) -> Self
}

public protocol FluentSQLQueryStatement {
static var insert: Self { get }
static var select: Self { get }
static var update: Self { get }
static var delete: Self { get }
var isInsert: Bool { get }
}
23 changes: 23 additions & 0 deletions Sources/FluentSQL/FluentSQLSchema.swift
@@ -0,0 +1,23 @@
public protocol FluentSQLSchema {
associatedtype Statement: FluentSQLSchemaStatement
associatedtype TableIdentifier: SQLTableIdentifier
associatedtype ColumnDefinition: SQLColumnDefinition
associatedtype TableConstraint: SQLTableConstraint
associatedtype ColumnIdentifier: SQLColumnIdentifier

var statement: Statement { get set }
var table: TableIdentifier { get set }
var columns: [ColumnDefinition] { get set }
var deleteColumns: [ColumnIdentifier] { get set }
var constraints: [TableConstraint] { get set }
var deleteConstraints: [TableConstraint] { get set }

static func schema(_ statement: Statement, _ table: TableIdentifier) -> Self
}


public protocol FluentSQLSchemaStatement {
static var createTable: Self { get }
static var alterTable: Self { get }
static var dropTable: Self { get }
}
6 changes: 6 additions & 0 deletions Sources/FluentSQL/QueryBuilder+GroupBy.swift
@@ -0,0 +1,6 @@
extension QueryBuilder where Database.Query: FluentSQLQuery, Result: SQLTable {
public func groupBy<T>(_ field: KeyPath<Result, T>) -> Self {
query.groupBy.append(.groupBy(.column(.keyPath(field))))
return self
}
}
41 changes: 41 additions & 0 deletions Sources/FluentSQL/SQL+Contains.swift
@@ -0,0 +1,41 @@
infix operator ~=
/// Has prefix
public func ~= <Result, D>(lhs: KeyPath<Result, String>, rhs: String) -> FilterOperator<D, Result>
where D: QuerySupporting, D.QueryFilterMethod: SQLBinaryOperator
{
return .make(lhs, .like, ["%" + rhs])
}
/// Has prefix
public func ~= <Result, D>(lhs: KeyPath<Result, String?>, rhs: String) -> FilterOperator<D, Result>
where D: QuerySupporting, D.QueryFilterMethod: SQLBinaryOperator
{
return .make(lhs, .like, ["%" + rhs])
}

infix operator =~
/// Has suffix.
public func =~ <Result, D>(lhs: KeyPath<Result, String>, rhs: String) -> FilterOperator<D, Result>
where D: QuerySupporting, D.QueryFilterMethod: SQLBinaryOperator
{
return .make(lhs, .like, [rhs + "%"])
}
/// Has suffix.
public func =~ <Result, D>(lhs: KeyPath<Result, String?>, rhs: String) -> FilterOperator<D, Result>
where D: QuerySupporting, D.QueryFilterMethod: SQLBinaryOperator
{
return .make(lhs, .like, [rhs + "%"])
}

infix operator ~~
/// Contains.
public func ~~ <Result, D>(lhs: KeyPath<Result, String>, rhs: String) -> FilterOperator<D, Result>
where D: QuerySupporting, D.QueryFilterMethod: SQLBinaryOperator
{
return .make(lhs, .like, ["%" + rhs + "%"])
}
/// Contains.
public func ~~ <Result, D>(lhs: KeyPath<Result, String?>, rhs: String) -> FilterOperator<D, Result>
where D: QuerySupporting, D.QueryFilterMethod: SQLBinaryOperator
{
return .make(lhs, .like, ["%" + rhs + "%"])
}
27 changes: 27 additions & 0 deletions Sources/FluentSQL/SQL+JoinSupporting.swift
@@ -0,0 +1,27 @@
extension JoinSupporting where
QueryJoin: SQLJoin,
QueryJoinMethod == QueryJoin.Method,
QueryJoin.Expression.ColumnIdentifier.TableIdentifier == QueryJoin.TableIdentifier
{
/// See `JoinSupporting`.
public static func queryJoin(_ method: QueryJoinMethod, base: QueryJoin.Expression.ColumnIdentifier, joined: QueryJoin.Expression.ColumnIdentifier) -> QueryJoin {
guard let table = joined.table else {
fatalError("Cannot join column without a table identifier: \(joined).")
}
return .join(method, table, .binary(.column(base), .equal, .column(joined)))
}
}

extension JoinSupporting where QueryJoinMethod: SQLJoinMethod {
/// See `JoinSupporting`.
public static var queryJoinMethodDefault: QueryJoinMethod {
return .default
}
}

extension JoinSupporting where Query: FluentSQLQuery, QueryJoin == Query.Join {
/// See `JoinSupporting`.
public static func queryJoinApply(_ join: QueryJoin, to query: inout Query) {
query.joins.append(join)
}
}