Skip to content

Commit

Permalink
Start work on SQL compilation
Browse files Browse the repository at this point in the history
- add SQL builder module and AST node classes
- add .toBeLikeQuery matcher
  • Loading branch information
maxbrunsfeld committed Dec 13, 2012
1 parent 1faa86d commit 89f7aa4
Show file tree
Hide file tree
Showing 15 changed files with 300 additions and 1 deletion.
5 changes: 4 additions & 1 deletion lib/server/index.coffee
Expand Up @@ -7,8 +7,11 @@ loader.configure(
globals: { Monarch, _ }
)

Monarch.Sql = {}
_.extend Monarch,
Sql: {}
resourceUrlSeparator: '_'

loader.require('./sql/literal')
loader.requireTree('.')

module.exports = Monarch
5 changes: 5 additions & 0 deletions lib/server/relations/relation.coffee
@@ -0,0 +1,5 @@
module.exports = ({ Monarch, _ }) ->

_.extend Monarch.Relations.Relation.prototype,
toSql: ->
(new Monarch.Sql.Builder).visit(this).toSql()
11 changes: 11 additions & 0 deletions lib/server/sql/binary_operator.coffee
@@ -0,0 +1,11 @@
module.exports = ({ Monarch, _ }) ->

class Monarch.Sql.BinaryOperator
constructor: (@left, @right, @operator) ->

toSql: ->
[
@left.toSql(),
@operator,
@right.toSql()
].join(' ')
75 changes: 75 additions & 0 deletions lib/server/sql/builder.coffee
@@ -0,0 +1,75 @@
module.exports = ({ Monarch, _ }) ->

class Monarch.Sql.Builder
visit: Monarch.Util.Visitor.visit

visit_Relations_Table: (r) ->
new Monarch.Sql.Query(
new Monarch.Sql.TableRef(r.resourceName()))

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

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

visit_Relations_Limit: (r) ->
_.tap(@visit(r.operand), (query) =>
query.limitCount = r.count)

visit_Relations_Offset: (r) ->
_.tap(@visit(r.operand), (query) =>
query.offsetCount = r.count)

visit_Relations_Union: (r) ->
new Monarch.Sql.Union(@visit(r.left), @visit(r.right))

visit_Relations_Difference: (r) ->
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(
new Monarch.Sql.JoinTableRef(
leftQuery.tableRef,
rightQuery.tableRef,
condition
))

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

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

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

visit_Expressions_OrderBy: (e) ->
new Monarch.Sql.OrderByExpression(
e.column.table.resourceName(),
e.column.resourceName(),
directionString(e.directionCoefficient))

visit_String: (e) ->
new Monarch.Sql.StringLiteral(e)

visit_Boolean: (e) ->
new Monarch.Sql.Literal(e)

visit_Number: (e) ->
new Monarch.Sql.Literal(e)

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

directionString = (coefficient) ->
if (coefficient == -1) then 'DESC' else 'ASC'
7 changes: 7 additions & 0 deletions lib/server/sql/column.coffee
@@ -0,0 +1,7 @@
module.exports = ({ Monarch, _ }) ->

class Monarch.Sql.Column
constructor: (@tableName, @name) ->

toSql: ->
""" "#{@tableName}"."#{@name}" """
7 changes: 7 additions & 0 deletions lib/server/sql/difference.coffee
@@ -0,0 +1,7 @@
module.exports = ({ Monarch, _ }) ->

class Monarch.Sql.Difference
constructor: (@left, @right) ->

toSql: ->
[@left.toSql(), "EXCEPT", @right.toSql()].join(' ')
13 changes: 13 additions & 0 deletions lib/server/sql/join_table_ref.coffee
@@ -0,0 +1,13 @@
module.exports = ({ Monarch, _ }) ->

class Monarch.Sql.JoinTableRef
constructor: (@left, @right, @condition) ->

toSql: ->
[
@left.toSql(),
"INNER JOIN",
@right.toSql(),
"ON",
@condition.toSql()
].join(' ')
7 changes: 7 additions & 0 deletions lib/server/sql/literal.coffee
@@ -0,0 +1,7 @@
module.exports = ({ Monarch, _ }) ->

class Monarch.Sql.Literal
constructor: (@value) ->

toSql: ->
@value.toString()
9 changes: 9 additions & 0 deletions lib/server/sql/order_by_expression.coffee
@@ -0,0 +1,9 @@
module.exports = ({ Monarch, _ }) ->

class Monarch.Sql.OrderByExpression
constructor: (@tableName, @columnName, @directionString) ->

toSql: ->
"""
"#{@tableName}"."#{@columnName}" #{@directionString}
"""
37 changes: 37 additions & 0 deletions lib/server/sql/query.coffee
@@ -0,0 +1,37 @@
module.exports = ({ Monarch, _ }) ->

class Monarch.Sql.Query
constructor: (@tableRef) ->
@condition = null
@orderByExpressions = []

toSql: ->
_.compact([
@selectClauseSql(),
@fromClauseSql(),
@whereClauseSql(),
@orderByClauseSql(),
@limitClauseSql(),
@offsetClauseSql()
]).join(' ')

selectClauseSql: ->
"SELECT *"

fromClauseSql: ->
"FROM " + @tableRef.toSql()

whereClauseSql: ->
"WHERE " + @condition.toSql() if @condition

orderByClauseSql: ->
if not _.isEmpty(@orderByExpressions)
"ORDER BY " + @orderByExpressions.map((e) -> e.toSql()).join(', ')

limitClauseSql: ->
if @limitCount
"LIMIT " + @limitCount

offsetClauseSql: ->
if @offsetCount
"OFFSET " + @offsetCount
5 changes: 5 additions & 0 deletions lib/server/sql/string_literal.coffee
@@ -0,0 +1,5 @@
module.exports = ({ Monarch, _ }) ->

class Monarch.Sql.StringLiteral extends Monarch.Sql.Literal
toSql: ->
"'" + super + "'"
8 changes: 8 additions & 0 deletions lib/server/sql/table_ref.coffee
@@ -0,0 +1,8 @@
module.exports = ({ Monarch, _ }) ->

class Monarch.Sql.TableRef
constructor: (@tableName) ->

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

7 changes: 7 additions & 0 deletions lib/server/sql/union.coffee
@@ -0,0 +1,7 @@
module.exports = ({ Monarch, _ }) ->

class Monarch.Sql.Union
constructor: (@left, @right) ->

toSql: ->
[@left.toSql(), "UNION", @right.toSql()].join(' ')
10 changes: 10 additions & 0 deletions spec/server/spec_helper.coffee
@@ -1,3 +1,13 @@
beforeEach ->
@addMatchers
toBeLikeQuery: (sql) ->
normalizeSql(@actual) == normalizeSql(sql)

normalizeSql = (string) ->
string
.replace(/\s+/g, ' ')
.replace(/[(\s*$)]/g, '')

module.exports =
Monarch: require "#{__dirname}/../../lib/server/index"
_: require "underscore"
95 changes: 95 additions & 0 deletions spec/server/sql/builder_spec.coffee
@@ -0,0 +1,95 @@
{ Monarch } = require "../spec_helper"

describe "Sql.Builder", ->
blogs = blogPosts = null

class Blog extends Monarch.Record
@extended(this)
@columns
public: 'boolean'
title: 'string'
authorId: 'integer'

class BlogPost extends Monarch.Record
@extended(this)
@columns
public: 'boolean'
blogId: 'integer'
title: 'string'

beforeEach ->
blogs = Blog.table
blogPosts = BlogPost.table

describe "tables", ->
it "constructs a table query", ->
expect(blogPosts.toSql()).toBeLikeQuery("""
SELECT * FROM "blog_posts"
""")

describe "selections", ->
it "constructs a query with the right WHERE clause", ->
relation = blogPosts.where({ public: true, blogId: 1 })
expect(relation.toSql()).toBeLikeQuery("""
SELECT * FROM "blog_posts"
WHERE "blog_posts"."public" = true AND "blog_posts"."blog_id" = 1
""")

it "quotes string literals correctly", ->
relation = blogPosts.where({ title: "Node Fibers and You" })
expect(relation.toSql()).toBeLikeQuery("""
SELECT * FROM "blog_posts"
WHERE "blog_posts"."title" = 'Node Fibers and You'
""")

describe "orderings", ->
it "constructs a query with a valid order by clause", ->
relation = blogPosts.orderBy("title desc")
expect(relation.toSql()).toBeLikeQuery("""
SELECT * FROM "blog_posts"
ORDER BY "blog_posts"."title" DESC, "blog_posts"."id" ASC
""")

describe "limits", ->
it "constructs a limit query", ->
relation = blogPosts.limit(5)
expect(relation.toSql()).toBeLikeQuery("""
SELECT * FROM "blog_posts" LIMIT 5
""")

it "constructs a limit query with an offset", ->
relation = blogPosts.limit(5, 2)
expect(relation.toSql()).toBeLikeQuery("""
SELECT * FROM "blog_posts" LIMIT 5 OFFSET 2
""")

describe "unions", ->
it "constructs a set union query", ->
left = blogPosts.where(blogId: 5)
right = blogPosts.where(public: true)
relation = left.union(right)
expect(relation.toSql()).toBeLikeQuery("""
SELECT * FROM "blog_posts" WHERE "blog_posts"."blog_id" = 5
UNION
SELECT * FROM "blog_posts" WHERE "blog_posts"."public" = true
""")

describe "differences", ->
it "constructs a set difference query", ->
left = blogPosts.where(blogId: 5)
right = blogPosts.where(public: true)
relation = left.difference(right)
expect(relation.toSql()).toBeLikeQuery("""
SELECT * FROM "blog_posts" WHERE "blog_posts"."blog_id" = 5
EXCEPT
SELECT * FROM "blog_posts" WHERE "blog_posts"."public" = true
""")

describe "joins", ->
it "constructs a join query", ->
relation = blogs.join(blogPosts)
expect(relation.toSql()).toBeLikeQuery("""
SELECT * FROM "blogs"
INNER JOIN "blog_posts"
ON "blogs"."id" = "blog_posts"."blog_id"
""")

0 comments on commit 89f7aa4

Please sign in to comment.