Skip to content

Commit

Permalink
Build subqueries for joins involving selections
Browse files Browse the repository at this point in the history
  • Loading branch information
maxbrunsfeld committed Dec 13, 2012
1 parent 8a9715d commit 6f050e0
Show file tree
Hide file tree
Showing 8 changed files with 229 additions and 73 deletions.
67 changes: 39 additions & 28 deletions lib/server/sql/builder.coffee
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
module.exports = ({ Monarch, _ }) ->

visitBinaryOperator = (operator) ->
(e, args...) ->
new Monarch.Sql.BinaryOperator(
@visit(e.left, args...),
@visit(e.right, args...),
operator)

class Monarch.Sql.Builder
constructor: ->
@subqueryIndex = 0

visit: Monarch.Util.Visitor.visit

visit_Relations_Table: (r) ->
tableRef = new Monarch.Sql.TableRef(r.resourceName())
columns = (@visit(column) for column in r.columns())
columns = (@visit(column, tableRef) for column in r.columns())
new Monarch.Sql.Query(select: columns, from: tableRef)

visit_Relations_Selection: (r) ->
_.tap(@visit(r.operand), (query) =>
query.condition = @visit(r.predicate))
query.condition = @visit(r.predicate, query.from))

visit_Relations_OrderBy: (r) ->
_.tap(@visit(r.operand), (query) =>
query.orderByExpressions = r.orderByExpressions.map (e) => @visit(e))
query.orderByExpressions = (@visit(e) for e in r.orderByExpressions))

visit_Relations_Limit: (r) ->
_.tap(@visit(r.operand), (query) =>
Expand All @@ -31,30 +41,34 @@ module.exports = ({ Monarch, _ }) ->
new Monarch.Sql.Difference(@visit(r.left), @visit(r.right))

visit_Relations_InnerJoin: (r) ->
leftQuery = @visit(r.left)
rightQuery = @visit(r.right)
condition = @visit(r.predicate)

new Monarch.Sql.Query(
select: leftQuery.select.concat(rightQuery.select)
from: new Monarch.Sql.JoinTableRef(
leftQuery.from,
rightQuery.from,
condition))
components = for side in ['left', 'right']
query = @visit(r[side])
if query.canHaveJoinAdded()
from = query.from
select = query.select
else
from = buildSubquery.call(this, query)
select = from.selectList()
{ from, select }

select = (components[0].select).concat(components[1].select)
join = new Monarch.Sql.JoinTableRef(components[0].from, components[1].from)
join.condition = @visit(r.predicate, join)
new Monarch.Sql.Query({ select, from: join })

visit_Relations_Projection: (r) ->
table = r.table
columns = (@visit(column) for column in table.columns())
_.tap(@visit(r.operand), (query) -> query.select = columns)

visit_Expressions_And: (e) ->
visitBinaryOperator.call(this, e, "AND")
_.tap(@visit(r.operand), (query) =>
columns = (@visit(column, query.from) for column in r.table.columns())
query.select = columns)

visit_Expressions_Equal: (e) ->
visitBinaryOperator.call(this, e, "=")
visit_Expressions_And: visitBinaryOperator("AND")
visit_Expressions_Equal: visitBinaryOperator("=")

visit_Expressions_Column: (e) ->
new Monarch.Sql.Column(e.table.resourceName(), e.resourceName())
visit_Expressions_Column: (e, tableRef) ->
new Monarch.Sql.Column(
tableRef,
e.table.resourceName()
e.resourceName())

visit_Expressions_OrderBy: (e) ->
new Monarch.Sql.OrderByExpression(
Expand All @@ -71,11 +85,8 @@ module.exports = ({ Monarch, _ }) ->
visit_Number: (e) ->
new Monarch.Sql.Literal(e)

visitBinaryOperator = (e, operator) ->
new Monarch.Sql.BinaryOperator(
@visit(e.left),
@visit(e.right),
operator)
buildSubquery = (query) ->
new Monarch.Sql.Subquery(query, ++@subqueryIndex)

directionString = (coefficient) ->
if (coefficient == -1) then 'DESC' else 'ASC'
30 changes: 25 additions & 5 deletions lib/server/sql/column.coffee
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
module.exports = ({ Monarch, _ }) ->

class Monarch.Sql.Column
constructor: (@tableName, @name) ->
@qualifyName: (tableName, columnName) ->
'"' + tableName + '"."' + columnName + '"'

@aliasName: (tableName, columnName) ->
"#{tableName}__#{columnName}"

constructor: (@source, @tableName, @name) ->

toSql: ->
"\"#{@tableName}\".\"#{@name}\""
@sourceName()

toSelectClauseSql: ->
@toSql() + " as " + @qualifiedName()
if @needsAlias()
"#{@sourceName()} as #{@aliasName()}"
else
@sourceName()

aliasName: ->
{ tableName, columnName } = @resolveName()
@constructor.aliasName(tableName, columnName)

sourceName: ->
{ tableName, columnName } = @resolveName()
@constructor.qualifyName(tableName, columnName)

needsAlias: ->
@resolveName().needsAlias

qualifiedName: ->
"#{@tableName}__#{@name}"
resolveName: ->
@_resolvedName or= @source.resolveColumnName(@tableName, @name)
3 changes: 3 additions & 0 deletions lib/server/sql/join_table_ref.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ module.exports = ({ Monarch, _ }) ->
class Monarch.Sql.JoinTableRef
constructor: (@left, @right, @condition) ->

resolveColumnName: (args...) ->
@left.resolveColumnName(args...) || @right.resolveColumnName(args...)

toSql: ->
[
@left.toSql(),
Expand Down
3 changes: 3 additions & 0 deletions lib/server/sql/query.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ module.exports = ({ Monarch, _ }) ->
offsetClauseSql: ->
if @offsetCount
"OFFSET " + @offsetCount

canHaveJoinAdded: ->
!@condition?
23 changes: 23 additions & 0 deletions lib/server/sql/subquery.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module.exports = ({ Monarch, _ }) ->

class Monarch.Sql.Subquery
constructor: (@query, index) ->
@name = "t" + index

resolveColumnName: (tableName, columnName) ->
innerNames = @query.from.resolveColumnName(tableName, columnName)
if innerNames
{
tableName: @name,
columnName: Monarch.Sql.Column.aliasName(
innerNames.tableName,
innerNames.columnName),
needsAlias: false,
}

selectList: ->
for column in @query.select
new Monarch.Sql.Column(this, column.tableName, column.name)

toSql: ->
"( #{@query.toSql()} ) as \"#{@name}\""
8 changes: 8 additions & 0 deletions lib/server/sql/table_ref.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ module.exports = ({ Monarch, _ }) ->

toSql: ->
'"' + @tableName + '"'

resolveColumnName: (tableName, columnName) ->
if tableName is @tableName
{
tableName: @tableName,
columnName: columnName,
needsAlias: true
}
70 changes: 47 additions & 23 deletions spec/server/db/record_retriever_spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -115,30 +115,54 @@ describe "Db.RecordRetriever", ->
done()

describe "joins", ->
it "builds composite tuples with the correct left and right records", (done) ->
blogs.join(blogPosts).all (err, tuples) ->
blogHashes = [
{ id: 1, public: true, title: 'Public Blog1', authorId: 1 }
{ id: 1, public: true, title: 'Public Blog1', authorId: 1 }
{ id: 2, public: true, title: 'Public Blog2', authorId: 1 }
{ id: 2, public: true, title: 'Public Blog2', authorId: 1 }
]

blogPostHashes = [
{ id: 1, public: true, title: 'Public Post1', blogId: 1 }
{ id: 3, public: false, title: 'Private Post1', blogId: 1 }
{ id: 2, public: true, title: 'Public Post2', blogId: 2 }
{ id: 4, public: false, title: 'Private Post2', blogId: 2 }
]
describe "a join between two tables", ->
it "builds composite tuples with the correct left and right records", (done) ->
blogs.join(blogPosts).all (err, tuples) ->
blogHashes = [
{ id: 1, public: true, title: 'Public Blog1', authorId: 1 }
{ id: 1, public: true, title: 'Public Blog1', authorId: 1 }
{ id: 2, public: true, title: 'Public Blog2', authorId: 1 }
{ id: 2, public: true, title: 'Public Blog2', authorId: 1 }
]

blogPostHashes = [
{ id: 1, public: true, title: 'Public Post1', blogId: 1 }
{ id: 3, public: false, title: 'Private Post1', blogId: 1 }
{ id: 2, public: true, title: 'Public Post2', blogId: 2 }
{ id: 4, public: false, title: 'Private Post2', blogId: 2 }
]

expect(tuples.length).toBe(4)
for tuple, i in tuples
expect(tuple).toBeA(Monarch.CompositeTuple)
expect(tuple.left).toBeA(Blog)
expect(tuple.left.fieldValues()).toEqual(blogHashes[i])
expect(tuple.right).toBeA(BlogPost)
expect(tuple.right.fieldValues()).toEqual(blogPostHashes[i])
done()

describe "a join between a selection and a table", ->
it "builds composite tuples with the correct left and right records", (done) ->
blogs.where(title: 'Public Blog1').join(blogPosts).all (err, tuples) ->
blogHashes = [
{ id: 1, public: true, title: 'Public Blog1', authorId: 1 }
{ id: 1, public: true, title: 'Public Blog1', authorId: 1 }
]

blogPostHashes = [
{ id: 1, public: true, title: 'Public Post1', blogId: 1 }
{ id: 3, public: false, title: 'Private Post1', blogId: 1 }
]

expect(tuples.length).toBe(2)
for tuple, i in tuples
expect(tuple).toBeA(Monarch.CompositeTuple)
expect(tuple.left).toBeA(Blog)
expect(tuple.left.fieldValues()).toEqual(blogHashes[i])
expect(tuple.right).toBeA(BlogPost)
expect(tuple.right.fieldValues()).toEqual(blogPostHashes[i])
done()

expect(tuples.length).toBe(4)
for tuple, i in tuples
expect(tuple).toBeA(Monarch.CompositeTuple)
expect(tuple.left).toBeA(Blog)
expect(tuple.left.fieldValues()).toEqual(blogHashes[i])
expect(tuple.right).toBeA(BlogPost)
expect(tuple.right.fieldValues()).toEqual(blogPostHashes[i])
done()

describe "projections", ->
it "builds a the right record class", (done) ->
Expand Down
98 changes: 81 additions & 17 deletions spec/server/sql/builder_spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -163,23 +163,87 @@ describe "Sql.Builder", ->
""")

describe "joins", ->
it "constructs a join query", ->
relation = blogs.join(blogPosts)
expect(relation.toSql()).toBeLikeQuery("""
SELECT
"blogs"."id" as blogs__id,
"blogs"."public" as blogs__public,
"blogs"."title" as blogs__title,
"blogs"."author_id" as blogs__author_id,
"blog_posts"."id" as blog_posts__id,
"blog_posts"."public" as blog_posts__public,
"blog_posts"."title" as blog_posts__title,
"blog_posts"."blog_id" as blog_posts__blog_id
FROM
"blogs" INNER JOIN "blog_posts"
ON
"blogs"."id" = "blog_posts"."blog_id"
""")
describe "a join between two tables", ->
it "constructs a join query", ->
relation = blogs.join(blogPosts)
expect(relation.toSql()).toBeLikeQuery("""
SELECT
"blogs"."id" as blogs__id,
"blogs"."public" as blogs__public,
"blogs"."title" as blogs__title,
"blogs"."author_id" as blogs__author_id,
"blog_posts"."id" as blog_posts__id,
"blog_posts"."public" as blog_posts__public,
"blog_posts"."title" as blog_posts__title,
"blog_posts"."blog_id" as blog_posts__blog_id
FROM
"blogs"
INNER JOIN
"blog_posts"
ON
"blogs"."id" = "blog_posts"."blog_id"
""")

describe "a join between a selection and a table", ->
it "makes a subquery for a selection on the left", ->
relation = blogs.where(public: true).join(blogPosts)
expect(relation.toSql()).toBeLikeQuery("""
SELECT
"t1"."blogs__id",
"t1"."blogs__public",
"t1"."blogs__title",
"t1"."blogs__author_id",
"blog_posts"."id" as blog_posts__id,
"blog_posts"."public" as blog_posts__public,
"blog_posts"."title" as blog_posts__title,
"blog_posts"."blog_id" as blog_posts__blog_id
FROM
(
SELECT
"blogs"."id" as blogs__id,
"blogs"."public" as blogs__public,
"blogs"."title" as blogs__title,
"blogs"."author_id" as blogs__author_id
FROM
"blogs"
WHERE
"blogs"."public" = true
) as "t1"
INNER JOIN
"blog_posts"
ON
"t1"."blogs__id" = "blog_posts"."blog_id"
""")

it "makes a subquery for a selection on the right", ->
relation = blogs.join(blogPosts.where(public: true))
expect(relation.toSql()).toBeLikeQuery("""
SELECT
"blogs"."id" as blogs__id,
"blogs"."public" as blogs__public,
"blogs"."title" as blogs__title,
"blogs"."author_id" as blogs__author_id,
"t1"."blog_posts__id",
"t1"."blog_posts__public",
"t1"."blog_posts__title",
"t1"."blog_posts__blog_id"
FROM
"blogs"
INNER JOIN
(
SELECT
"blog_posts"."id" as blog_posts__id,
"blog_posts"."public" as blog_posts__public,
"blog_posts"."title" as blog_posts__title,
"blog_posts"."blog_id" as blog_posts__blog_id
FROM
"blog_posts"
WHERE
"blog_posts"."public" = true
) as "t1"
ON
"blogs"."id" = "t1"."blog_posts__blog_id"
""")

describe "projections", ->
it "constructs a projected join query", ->
Expand Down

0 comments on commit 6f050e0

Please sign in to comment.