Skip to content

Commit

Permalink
New features:
Browse files Browse the repository at this point in the history
1) Support for EXISTS.
2) Support for HAVING.
3) Support for ArelHelpers (option now yields Article[:id] instead of Article.arel_table[:id]).
4) Option to remove Arel::Nodes namespace.
  • Loading branch information
camertron committed Sep 1, 2015
1 parent 04db46f commit 3517dc7
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 22 deletions.
19 changes: 15 additions & 4 deletions lib/scuttle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require Pathname(__FILE__).dirname.dirname.join("vendor/jars/Scuttle.jar").to_s

java_import 'com.camertron.Scuttle.SqlStatementVisitor'
java_import 'com.camertron.Scuttle.ScuttleOptions'
java_import 'com.camertron.Scuttle.Resolver.AssociationManager'
java_import 'com.camertron.Scuttle.Resolver.AssociationType'
java_import 'com.camertron.SQLParser.SQLLexer'
Expand All @@ -22,15 +23,18 @@ class << self

MAX_CHARS = 50

def convert(sql_string, assoc_manager)
def convert(sql_string, options = {}, assoc_manager)
input = ANTLRInputStream.new(sql_string)
lexer = SQLLexer.new(input)
tokens = CommonTokenStream.new(lexer)
parser = SQLParser.new(tokens)
visitor = SqlStatementVisitor.new(assoc_manager.createResolver)
options = scuttle_options_from(options)
visitor = SqlStatementVisitor.new(assoc_manager.createResolver, options)
visitor.visit(parser.sql)
visitor.toString
rescue
rescue => e
puts e.message
puts e.backtrace
raise ScuttleConversionError, 'Scuttle parser error, check your SQL syntax.'
end

Expand All @@ -45,6 +49,13 @@ def colorize(str, encoder = :terminal)

private

def scuttle_options_from(options)
ScuttleOptions.new.tap do |scuttle_options|
scuttle_options.useArelHelpers(options.fetch(:use_arel_helpers, false))
scuttle_options.useArelNodesPrefix(options.fetch(:use_arel_nodes_prefix, true))
end
end

def parse(str)
tokens = str.split(/([\[\]()])/)
consumed, children = build_tree(tokens, 0)
Expand Down Expand Up @@ -156,4 +167,4 @@ def is_pair?(open, close)
end

end
end
end
24 changes: 12 additions & 12 deletions spec/java/unit/join_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def with_arel_select(statement)
end

it "identifies non-nested ActiveRecord associations" do
expect(convert(with_select("INNER JOIN comments ON comments.post_id = posts.id"), manager)).to eq(
expect(convert(with_select("INNER JOIN comments ON comments.post_id = posts.id"), {}, manager)).to eq(
with_arel_select(":comments")
)
end
Expand All @@ -54,7 +54,7 @@ def with_arel_select(statement)
with_select(
"INNER JOIN comments ON comments.post_id = posts.id " +
"INNER JOIN authors ON authors.id = comments.author_id"
), manager
), {}, manager
)).to eq(with_arel_select(":comments => :author"))
end

Expand All @@ -65,39 +65,39 @@ def with_arel_select(statement)
"INNER JOIN posts ON posts.id = comments.post_id " +
"INNER JOIN favorites ON favorites.post_id = posts.id"

expect(convert(query, manager)).to eq(
expect(convert(query, {}, manager)).to eq(
"Author.select(Arel.star).joins(:comment => { :post => :favorites })"
)
end

it "works with has_and_belongs_to_many associations" do
query = "SELECT authors.* FROM authors " +
query = "SELECT authors.* FROM authors " +
"INNER JOIN authors_collab_posts ON authors_collab_posts.author_id = authors.id " +
"INNER JOIN collab_posts ON collab_posts.id = authors_collab_posts.collab_post_id"

expect(convert(query, manager)).to eq(
expect(convert(query, {}, manager)).to eq(
"Author.select(Author.arel_table[Arel.star]).joins(:collab_posts)"
)
end

it "works with has_and_belongs_to_many associations in the opposite direction" do
query = "SELECT collab_posts.* FROM collab_posts " +
"INNER JOIN authors_collab_posts ON authors_collab_posts.collab_post_id = collab_posts.id " +
"INNER JOIN authors_collab_posts ON authors_collab_posts.collab_post_id = collab_posts.id " +
"INNER JOIN authors ON authors.id = authors_collab_posts.author_id"

expect(convert(query, manager)).to eq(
expect(convert(query, {}, manager)).to eq(
"CollabPost.select(CollabPost.arel_table[Arel.star]).joins(:authors)"
)
end

it "falls back to arel upon encountering an unsupported join type" do
expect(convert(with_select("LEFT OUTER JOIN comments ON comments.post_id = posts.id"), manager)).to eq(
expect(convert(with_select("LEFT OUTER JOIN comments ON comments.post_id = posts.id"), {}, manager)).to eq(
with_arel_select("Post.arel_table.join(Comment.arel_table, Arel::Nodes::OuterJoin).on(Comment.arel_table[:post_id].eq(Post.arel_table[:id])).join_sources")
)
end

it "falls back to arel if the association can't be recognized" do
expect(convert(with_select("INNER JOIN comments ON comments.body = posts.id"), manager)).to eq(
expect(convert(with_select("INNER JOIN comments ON comments.body = posts.id"), {}, manager)).to eq(
with_arel_select("Post.arel_table.join(Comment.arel_table).on(Comment.arel_table[:body].eq(Post.arel_table[:id])).join_sources")
)
end
Expand All @@ -112,11 +112,11 @@ def with_arel_select(statement)
end

it "identifies joins that use a custom foreign key" do
expect(convert(with_select("INNER JOIN comments ON posts.id = comments.my_post_id"), manager)).to eq(
expect(convert(with_select("INNER JOIN comments ON posts.id = comments.my_post_id"), {}, manager)).to eq(
with_arel_select(":comments")
)

expect(convert("SELECT * FROM comments INNER JOIN posts ON comments.my_post_id = posts.id", manager)).to eq(
expect(convert("SELECT * FROM comments INNER JOIN posts ON comments.my_post_id = posts.id", {}, manager)).to eq(
"Comment.select(Arel.star).joins(:post)"
)
end
Expand All @@ -131,7 +131,7 @@ def with_arel_select(statement)
end

it "uses the custom association name instead of the table name in the join" do
expect(convert(with_select("INNER JOIN comments ON posts.id = comments.post_id"), manager)).to eq(
expect(convert(with_select("INNER JOIN comments ON posts.id = comments.post_id"), {}, manager)).to eq(
with_arel_select(":utterances")
)
end
Expand Down
18 changes: 15 additions & 3 deletions spec/java/unit/select_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@

it "works with multiple qualified and non-qualified columns" do
expect(convert("SELECT id, phrases.meta_key, key from phrases")).to eq(
"Phrase.select(:id, Phrase.arel_table[:meta_key], :key)"
"Phrase.select([:id, Phrase.arel_table[:meta_key], :key])"
)
end

it "works with multiple columns" do
expect(convert("SELECT phrases.key, translations.text FROM phrases")).to eq(
"Phrase.select(Phrase.arel_table[:key], Translation.arel_table[:text])"
"Phrase.select([Phrase.arel_table[:key], Translation.arel_table[:text]])"
)
end

Expand Down Expand Up @@ -91,7 +91,7 @@

it "works with a crazy example that ties all this together" do
expect(convert("SELECT COALESCE(1, 'a', (phrases.key + 1)) AS `col`, COUNT(*), STRLEN(phrases.key), phrases.created_at FROM phrases")).to eq(
"Phrase.select(Arel::Nodes::NamedFunction.new('COALESCE', [1, 'a', Arel::Nodes::Group.new(Phrase.arel_table[:key] + 1)]).as('col'), Arel.star.count, Arel::Nodes::NamedFunction.new('STRLEN', [Phrase.arel_table[:key]]), Phrase.arel_table[:created_at])"
"Phrase.select([Arel::Nodes::NamedFunction.new('COALESCE', [1, 'a', Arel::Nodes::Group.new(Phrase.arel_table[:key] + 1)]).as('col'), Arel.star.count, Arel::Nodes::NamedFunction.new('STRLEN', [Phrase.arel_table[:key]]), Phrase.arel_table[:created_at]])"
)
end

Expand All @@ -106,4 +106,16 @@
"Phrase.select(:id).uniq"
)
end

it "doesn't use the Arel::Nodes namespace when option is given" do
expect(convert("SELECT COALESCE(phrases.key, 1, 'abc') FROM phrases", use_arel_nodes_prefix: false)).to eq(
"Phrase.select(NamedFunction.new('COALESCE', [Phrase.arel_table[:key], 1, 'abc']))"
)
end

it "uses ArelHelpers when option is given" do
expect(convert("SELECT phrases.key FROM phrases", use_arel_helpers: true)).to eq(
"Phrase.select(Phrase[:key])"
)
end
end
20 changes: 19 additions & 1 deletion spec/java/unit/where_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,15 @@ def with_arel_select(statement)
)
end

it "works with a single-element IN list" do
expect(convert(with_select("WHERE phrases.id IN (1)"))).to eq(
with_arel_select("Phrase.arel_table[:id].in(1)")
)
end

it "works with a simple IN list" do
expect(convert(with_select("WHERE phrases.id IN (1, 2, 3, 4)"))).to eq(
with_arel_select("Phrase.arel_table[:id].in(1, 2, 3, 4)")
with_arel_select("Phrase.arel_table[:id].in([1, 2, 3, 4])")
)
end

Expand All @@ -134,4 +140,16 @@ def with_arel_select(statement)
with_arel_select("Arel::Nodes::Between.new(Phrase.arel_table[:id], (Phrase.arel_table[:id] + 1).and(Phrase.arel_table[:id] + 2))")
)
end

it "supports EXISTS subqueries" do
expect(convert(with_select("WHERE EXISTS (SELECT * FROM phrases WHERE id = 1)"))).to eq(
"Phrase.select(Arel.star).where(Phrase.select(Arel.star).where(Phrase.arel_table[:id].eq(1)).exists)"
)
end

it "supports HAVING clauses" do
expect(convert(with_select("HAVING COUNT(*) > 5"))).to eq(
"Phrase.select(Arel.star).having(Arel.star.count.gt(5))"
)
end
end
4 changes: 2 additions & 2 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
config.mock_with :rr
end

def convert(sql_string, assoc_manager = AssociationManager.new)
Scuttle.convert(sql_string, assoc_manager)
def convert(sql_string, options = {}, assoc_manager = AssociationManager.new)
Scuttle.convert(sql_string, options, assoc_manager)
end
Binary file modified vendor/jars/Scuttle.jar
Binary file not shown.

0 comments on commit 3517dc7

Please sign in to comment.