5 Refactoring Recipes
This section is not meant to be read straight through, unless you are very studious.
A green identifier like Team indicates something that is being added to define-schema. A red highlight like (foo (bar x)) is used to help you follow a piece of code as it gets relocated, possibly with small adjustments.
5.1 Elemental Recipes
5.1.1 Join -> Expression
; original version: (from x TableX .... (join y TableY ....) ....) ; refactored version: (from x TableX .... (define y (join y TableY #:to x ....)) ....)
The refactored code is bigger! Is this a step in the wrong direction? If we stop refactoring here, you could argue that it is. The point of this recipe is to set up future refactoring. In the original version, there was an unwritten #:to x that would be lost if we relocated the code. By making #:to x explicit, we can now relocate the code using normal Racket techniques.
5.1.2 Expression -> Procedure
; original version: (from x TableX .... (select (foo (bar x) (baz x))) ....) ; refactored version: (define (NEW-PROC x) (foo (bar x) (baz x))) (from x TableX .... (select (NEW-PROC x)) ....)
(define/contract (NEW-PROC x) (-> (instanceof TableX) Number?) (foo (bar x) (baz x)))
; original version: (from x TableX .... (define y (join y TableY #:to x (join-on (.= (YID y) (YID x))))) ....) ; refactored version: (define/contract (Y-given-X x) (-> (instanceof TableX) (instanceof TableY)) (join y TableY #:to x (join-on (.= (YID y) (YID x))))) (from x TableX .... (define y (Y-given-X x)) ....)
5.1.3 Procedure -> define-schema
(define/contract (NEW-PROC x) (-> (instanceof TableX) any/c) (foo (bar x) (baz x)))
It accepts one argument. (The argument is x in this example.)
That argument is an instanceof a table from your schema definition. (The table is TableX in this example.)
#:property if the return value is a Scalar? (or one of its subtypes)
#:has-group if the return value is a singular grouped join. "Singular" means that adding the join to a query of TableX will not increase the number of rows that will be returned in the result set. "Grouped" means that the join contains at least one group-by clause.
#:has-one if the return value is a singular simple join. "Singular" means that adding the join to a query of TableX will not increase the number of rows that will be returned in the result set. "Simple" means that each of the join’s clauses is either a join-on or join-type clause.
(define-schema my-schema .... (table TableX .... #:property [NEW-PROC (foo (bar this) (baz this))] ....) ....)
Warning: this recipe is not complete! Continue reading the following subsections.
; Notice that `this` is used in a strict comparison... (.= (foo bar) (foo this)) ; ... and surround it with a fallback: (.= (foo bar) (?? (foo this) /void))
For the purposes of the Using define-schema walkthrough, you can just always add the /void fallback as seen above and move on. Or if you are not satisfied with this hand-waving, you should first read Nullability and then this May Be Null.
Alternatively, you don’t have to add the fallback now. If your code worked without a fallback prior to applying this recipe, it will still work without a fallback after applying this recipe. But future callers of this procedure might get an error.
(from p Player ; (Team p) returns a 'left join ... (join t (Team p) ; ... but we can override that here: (join-type 'inner)) ....)
Note that almost every #:has-group relationship should be a left join, because a group containing zero members is considered a failed join and unless it is a left join, rows will be filtered from the result.
5.1.4 Join <-> Define
(from x TableX .... (join y (TableY x)) ....) ; is almost equivalent to (from x TableX .... (define y (TableY x)) ....)
When y is joined, the join is immediately added to the query and is guaranteed to appear in the generated SQL.
When y is defined, the join is not immediately added to the query. If y is used as content in some clauses that follow, it will be added to the query at that time and both versions become equivalent. But if y is an unused definition, it essentially does not exist and both versions are not equivalent.
5.2 Compound Recipes
These recipes use one or more of the Elemental Recipes.
5.2.1 Singular Join -> Schema Definition
This recipe moves a singular join into define-schema.
Caution: In this example, the single-argument procedure Team happens to share its name with the existing table Team. This name-sharing is very common with singular joins, but not required.
; current code: (from p Player .... (join t Team (join-on (.= (TeamID t) (TeamID p)))) ....) ; desired code: (from p Player .... (join t (Team p)) ....)
(define/contract (NEW-PROC p) (-> (instanceof Player) (instanceof Team)) (join t Team #:to p (join-on (.= (TeamID t) (TeamID p))))) (from p Player .... (define t (NEW-PROC p)) ....)
(define-schema .... (table Player .... #:has-one [Team (join t Team (join-on (.= (TeamID t) (?? (TeamID this) /void))))] ....) ....) (from p Player .... (define t (Team p)) ....)
5.2.2 Grouped Join -> Schema Definition
; current code: (from t Team .... (join playersG Player (group-by (TeamID playersG)) (join-on (.= (TeamID playersG) (TeamID t)))) ....) ; desired code: (from t Team .... (join playersG (PlayersG t)) ....)
(from t Team .... (define playersG (join playersG Player #:to t (group-by (TeamID playersG)) (join-on (.= (TeamID playersG) (TeamID t))))) ....)
(define/contract (NEW-PROC t) (-> (instanceof Team) (instanceof Player)) (join playersG Player #:to t (group-by (TeamID playersG)) (join-on (.= (TeamID playersG) (TeamID t))))) (from t Team .... (define playersG (NEW-PROC t)) ....)
(define-schema .... (table Team .... #:has-group [PlayersG (join playersG Player (group-by (TeamID playersG)) (join-on (.= (TeamID playersG) (?? (TeamID this) /void))))] ....) ....) (from t Team .... (define playersG (PlayersG t)) ....)
And this recipe is complete.
5.2.3 Scalar -> Schema Definition
; current code: (from p Player .... (select (./ (ShotsMade p) (ShotsTaken p))) ....) ; desired code: (from p Player .... (select (ShootingPercentage p)) ....)
(define/contract (NEW-PROC p) (-> (instanceof Player) Scalar?) (./ (ShotsMade p) (ShotsTaken p))) (from p Player .... (select (NEW-PROC p)) ....)
(define-schema .... (table Player .... #:property [ShootingPercentage (./ (ShotsMade this) (ShotsTaken this))] ....) ....) (from p Player .... (select (ShootingPercentage p)) ....)
And this recipe is complete.
5.2.4 Scalar Flattening
; current code: (from p Player .... (select (TeamName (Team p))) ....) ; desired code: (from p Player .... (select (TeamName p)) ....)
(define/contract (NEW-PROC p) (-> (instanceof Player) Scalar?) (TeamName (Team p))) (from p Player .... (select (NEW-PROC p)) ....)
(define-schema .... (table Player .... #:property [TeamName (TeamName (Team this))] ....) ....) (from p Player .... (select (TeamName p)) ....)
And this recipe is complete.
5.2.5 Inline Join
; current code: (from p Player .... (join t (Team p)) .... (select (TeamName t)) ....) ; desired code: (from p Player .... ; this code gets removed: (join t (Team p)) .... (select (TeamName (Team p))) ....)
(from p Player .... ; this code is removed: (define t (Team p)) .... (select (TeamName (Team p))) ....)
And this recipe is complete. They key point is that if we proceed to use the Expression -> Procedure recipe on (TeamName (Team p)), the resulting procedure will now accept one argument which is an (instanceof Player). In the original version, it would have wanted an (instanceof Team).
5.2.6 Name Clarification
; current code: (from t Team .... (select (Name t)) ....) ; desired code: (from t Team .... (select (TeamName t)) ....)
(define/contract (NEW-PROC t) (-> (instanceof Team) Scalar?) (Name t)) (from t Team .... (select (NEW-PROC t)) ....)
(define-schema .... (table Team .... #:property [TeamName (Name this)] ....) ....) (from t Team .... (select (TeamName t)) ....)
And we are done.
Be aware of this to avoid breaking any existing call sites that depend on the original name appearing in the result set. The examples in this documentation ignore this caveat because this recipe is always used on a brand new query that has no call sites yet.