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

Add querying and additional context to the enhanced Arel AST #106

Merged
merged 24 commits into from
Jul 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
24bc523
WIP: Add query methods to the transformer AST
mvgijssel Jul 24, 2019
43b1ec4
Merge branch 'master' into 103/query-arel-transformer-ast
mvgijssel Jul 26, 2019
18a006a
Add contextual information to transformer nodes
mvgijssel Jul 26, 2019
1354ce9
Extracted context enhancer for Arel::Table
mvgijssel Jul 26, 2019
21bd87a
Sort rubocop
mvgijssel Jul 26, 2019
71ad35e
Fixed lint issues
mvgijssel Jul 26, 2019
0a1e052
Moved the context enhancer spec
mvgijssel Jul 26, 2019
8dee854
Fixed exception message in ArelTable context enhancer
mvgijssel Jul 26, 2019
14ab884
Fix comparison for select,update,insert and delete manager
mvgijssel Jul 28, 2019
b49a7e4
Added .query method to Node
mvgijssel Jul 28, 2019
852f878
Added AddchemaToTable transformer
mvgijssel Jul 28, 2019
c10744e
Add the required require
mvgijssel Jul 28, 2019
89021d5
Hack to support context argument
mvgijssel Jul 29, 2019
efb24ba
Add support for schemas in function calls
mvgijssel Jul 30, 2019
1b0e0cf
Added specs for updated tree managers
mvgijssel Jul 30, 2019
5501264
Added specs for the error branches of FuncCall
mvgijssel Jul 30, 2019
9380796
Add missing attributes to the SelectCore dot visitor
mvgijssel Jul 30, 2019
31c0503
Added TODO to Arel middleware
mvgijssel Jul 30, 2019
bd15cf2
Added specs for ArelTable context enhancer
mvgijssel Jul 30, 2019
efd17cb
Remove TODO
mvgijssel Jul 30, 2019
6c0b4f9
Remove comments
mvgijssel Jul 30, 2019
870aef1
Added specs for Query
mvgijssel Jul 30, 2019
8d7226b
Updated specs for AddSchemaToTable
mvgijssel Jul 30, 2019
cde022d
Remove broken spec
mvgijssel Jul 30, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,12 @@ AllCops:
- 'gemfiles/arel_gems.gemfile'
- 'gemfiles/default.gemfile'

Style/FrozenStringLiteralComment:
Enabled: false

Bundler/OrderedGems:
Enabled: false

Gemspec/OrderedDependencies:
Enabled: false

Style/MultilineBlockChain:
Enabled: false

Metrics/LineLength:
Enabled: true
Max: 100
Expand All @@ -31,12 +25,21 @@ Metrics/BlockLength:
Style/Documentation:
Enabled: false

Style/MultilineBlockChain:
Enabled: false

Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: comma

Style/TrailingCommaInArrayLiteral:
EnforcedStyleForMultiline: comma

Style/TrailingCommaInArguments:
EnforcedStyleForMultiline: comma

Style/FrozenStringLiteralComment:
Enabled: false

Layout/MultilineMethodCallIndentation:
Enabled: true
EnforcedStyle: indented
2 changes: 1 addition & 1 deletion lib/arel/extensions/delete_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
module Arel
class DeleteManager < Arel::TreeManager
def ==(other)
@ast == other.ast && @ctx == other.ctx
other.is_a?(self.class) && @ast == other.ast && @ctx == other.ctx
end

protected
Expand Down
2 changes: 1 addition & 1 deletion lib/arel/extensions/delete_statement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

module Arel
module Nodes
# https://www.postgresql.org/docs/9.5/sql-insert.html
# https://www.postgresql.org/docs/10/sql-delete.html
class DeleteStatement
module DeleteStatementExtension
attr_accessor :using
Expand Down
3 changes: 3 additions & 0 deletions lib/arel/extensions/function.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ module FunctionExtension
attr_accessor :filter
attr_accessor :within_group
attr_accessor :variardic
# postgres only: https://www.postgresql.org/docs/10/ddl-schemas.html
attr_accessor :schema_name

def initialize(expr, aliaz = nil)
super
Expand All @@ -31,6 +33,7 @@ class ToSql
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/AbcSize
def aggregate(name, o, collector)
collector << "#{o.schema_name}." if o.schema_name
collector << "#{name}("
collector << 'DISTINCT ' if o.distinct
collector << 'VARIADIC ' if o.variardic
Expand Down
2 changes: 1 addition & 1 deletion lib/arel/extensions/insert_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
module Arel
class InsertManager < Arel::TreeManager
def ==(other)
@ast == other.ast
other.is_a?(self.class) && @ast == other.ast
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/arel/extensions/select_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
module Arel
class SelectManager
def ==(other)
@ast == other.ast && @ctx == other.ctx
other.is_a?(self.class) && @ast == other.ast && @ctx == other.ctx
end

protected
Expand Down
3 changes: 3 additions & 0 deletions lib/arel/extensions/select_statement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ module SelectStatementExtension
def visit_Arel_Nodes_SelectStatement(o)
super

visit_edge o, 'lock'
mvgijssel marked this conversation as resolved.
Show resolved Hide resolved
visit_edge o, 'with'
visit_edge o, 'union'
visit_edge o, 'values_lists'
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/arel/extensions/table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Table
module TableExtension
# postgres only: https://www.postgresql.org/docs/9.5/sql-select.html
attr_accessor :only
# postgres only: https://www.postgresql.org/docs/9.5/ddl-schemas.html
# postgres only: https://www.postgresql.org/docs/10/ddl-schemas.html
attr_accessor :schema_name
# postgres only: https://www.postgresql.org/docs/9.1/catalog-pg-class.html
attr_accessor :relpersistence
Expand Down
2 changes: 1 addition & 1 deletion lib/arel/extensions/update_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
module Arel
class UpdateManager < Arel::TreeManager
def ==(other)
@ast == other.ast && @ctx == other.ctx
other.is_a?(self.class) && @ast == other.ast && @ctx == other.ctx
end

protected
Expand Down
21 changes: 15 additions & 6 deletions lib/arel/sql_to_arel/pg_query_visitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -427,11 +427,20 @@ def visit_FuncCall(
[Arel::Nodes::Overlaps.new(start1, end1, start2, end2)]

else
if function_names.length > 1
boom "Don't know how to handle function names `#{function_names}`"
end
case function_names.length
when 2
if function_names.first == PG_CATALOG
boom "Missing postgres function `#{function_names.last}`"
end

Arel::Nodes::NamedFunction.new(function_names.first, args)
func = Arel::Nodes::NamedFunction.new(function_names.last, args)
func.schema_name = function_names.first
func
when 1
Arel::Nodes::NamedFunction.new(function_names.first, args)
else
boom "Don't know how to handle function names length `#{function_names.length}`"
end
end

func.distinct = (agg_distinct.nil? ? false : true) unless func.is_a?(::Array)
Expand Down Expand Up @@ -540,12 +549,12 @@ def visit_LockingClause(strength:, wait_policy:)
1 => 'FOR KEY SHARE',
2 => 'FOR SHARE',
3 => 'FOR NO KEY UPDATE',
4 => 'FOR UPDATE'
4 => 'FOR UPDATE',
}.fetch(strength)
wait_policy_clause = {
0 => '',
1 => ' SKIP LOCKED',
2 => ' NOWAIT'
2 => ' NOWAIT',
}.fetch(wait_policy)

Arel::Nodes::Lock.new Arel.sql("#{strength_clause}#{wait_policy_clause}")
Expand Down
2 changes: 1 addition & 1 deletion lib/arel/sql_to_arel/pg_query_visitor/frame_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def arel(frame_options, start_offset, end_offset)
'FRAMEOPTION_START_VALUE_PRECEDING' => 0x00400,
'FRAMEOPTION_END_VALUE_PRECEDING' => 0x00800,
'FRAMEOPTION_START_VALUE_FOLLOWING' => 0x01000,
'FRAMEOPTION_END_VALUE_FOLLOWING' => 0x02000
'FRAMEOPTION_END_VALUE_FOLLOWING' => 0x02000,
}.freeze

def biggest_detractable_number(number, candidates)
Expand Down
3 changes: 3 additions & 0 deletions lib/arel/transformer.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
require_relative './transformer/node'
require_relative './transformer/path'
require_relative './transformer/path_node'
require_relative './transformer/query'
require_relative './transformer/visitor'

require_relative './transformer/add_schema_to_table'

module Arel
module Transformer
end
Expand Down
26 changes: 26 additions & 0 deletions lib/arel/transformer/add_schema_to_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Arel
module Transformer
class AddSchemaToTable
attr_reader :schema_name

def initialize(schema_name)
@schema_name = schema_name
end

# https://github.com/mvgijssel/arel_toolkit/issues/110
def call(arel, _context)
tree = Arel.transformer(arel)

tree.query(
class: Arel::Table,
schema_name: nil,
context: { range_variable: true },
).each do |node|
node['schema_name'].replace(schema_name)
end

tree.object
end
end
end
end
75 changes: 75 additions & 0 deletions lib/arel/transformer/context_enhancer/arel_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
module Arel
module Transformer
module ContextEnhancer
class ArelTable
# rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/AbcSize
def self.call(node)
mvgijssel marked this conversation as resolved.
Show resolved Hide resolved
mvgijssel marked this conversation as resolved.
Show resolved Hide resolved
context = node.context.merge!(range_variable: false, column_reference: false)
parent_object = node.parent.object

# Using Arel::Table as SELECT ... FROM <table>
if parent_object.is_a?(Arel::Nodes::JoinSource)
context[:range_variable] = true

# Using Arel::Table as SELECT ... FROM [<table>]
elsif parent_object.is_a?(Array) &&
node.parent.parent.object.is_a?(Arel::Nodes::JoinSource)
context[:range_variable] = true

# Using Arel::Table as SELECT ... INNER JOIN <table> ON TRUE
elsif parent_object.is_a?(Arel::Nodes::Join)
context[:range_variable] = true

# Using Arel::Table as an attribute SELECT <table>.id ...
elsif parent_object.is_a?(Arel::Attributes::Attribute)
context[:column_reference] = true

# Using Arel::Table in an INSERT INTO <table>
elsif parent_object.is_a?(Arel::Nodes::InsertStatement)
context[:range_variable] = true

# Using Arel::Table in an UPDATE <table> ...
elsif parent_object.is_a?(Arel::Nodes::UpdateStatement)
context[:range_variable] = true

# Arel::Table in UPDATE ... FROM [<table>]
elsif parent_object.is_a?(Array) &&
node.parent.parent.object.is_a?(Arel::Nodes::UpdateStatement)
context[:range_variable] = true

# Using Arel::Table in an DELETE FROM <table>
elsif parent_object.is_a?(Arel::Nodes::DeleteStatement)
context[:range_variable] = true

# Arel::Table in DELETE ... USING [<table>]
elsif parent_object.is_a?(Array) &&
node.parent.parent.object.is_a?(Arel::Nodes::DeleteStatement)
context[:range_variable] = true

# Using Arel::Table as an "alias" for WITH <table> AS (SELECT 1) SELECT 1
elsif parent_object.is_a?(Arel::Nodes::As) &&
node.parent.parent.parent.object.is_a?(Arel::Nodes::With)
context[:alias] = true

# Using Arel::Table as an "alias" for WITH RECURSIVE <table> AS (SELECT 1) SELECT 1
elsif parent_object.is_a?(Arel::Nodes::As) &&
node.parent.parent.parent.object.is_a?(Arel::Nodes::WithRecursive)
context[:alias] = true

# Using Arel::Table as an "alias" for SELECT INTO <table> ...
elsif parent_object.is_a?(Arel::Nodes::Into)
context[:alias] = true

else
raise "Unknown AST location for table #{node.inspect}, #{node.root_node.to_sql}"
end
end
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/AbcSize
end
end
end
end
17 changes: 16 additions & 1 deletion lib/arel/transformer/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Node
attr_reader :fields
attr_reader :children
attr_reader :root_node
attr_reader :context

def initialize(object)
@object = object
Expand All @@ -15,6 +16,7 @@ def initialize(object)
@fields = []
@children = {}
@dirty = false
@context = {}
end

def inspect
Expand Down Expand Up @@ -63,7 +65,7 @@ def add(path_node, node)
def to_sql(engine = Table.engine)
return nil if children.empty?

target_object = object.is_a?(Arel::SelectManager) ? object.ast : object
target_object = object.is_a?(Arel::TreeManager) ? object.ast : object
collector = Arel::Collectors::SQLString.new
collector = engine.connection.visitor.accept target_object, collector
collector.value
Expand All @@ -73,6 +75,19 @@ def [](key)
@children.fetch(key)
end

def child_at_path(path_items)
selected_node = self
path_items.each do |path_item|
selected_node = selected_node[path_item]
return nil if selected_node.nil?
end
selected_node
end

def query(**kwargs)
Arel::Transformer::Query.call(self, kwargs)
end

protected

attr_writer :path
Expand Down
2 changes: 1 addition & 1 deletion lib/arel/transformer/path_node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def arguments?
def inspect
case value
when String
"\"#{value}\""
"'#{value}'"
else
value.inspect
end
Expand Down
36 changes: 36 additions & 0 deletions lib/arel/transformer/query.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Arel
module Transformer
class Query
def self.call(node, kwargs)
node_attributes = %i[context parent]
node_args = kwargs.slice(*node_attributes)
object_args = kwargs.except(*node_attributes)

node.each.select do |child_node|
next unless matches?(child_node, node_args)

matches?(child_node.object, object_args)
end
end

def self.matches?(object, test)
mvgijssel marked this conversation as resolved.
Show resolved Hide resolved
case test
when Hash
case object
when Hash
test <= object
else
test.all? do |test_key, test_value|
next false unless object.respond_to?(test_key)

object_attribute_value = object.public_send(test_key)
matches? object_attribute_value, test_value
end
end
else
object == test
end
end
end
end
end
Loading