From 771e793a2a6a94857aeb4983b0982212d8267054 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Fri, 14 Jun 2019 15:38:49 -0600 Subject: [PATCH 01/48] rewrite wip start --- Gemfile.lock | 2 +- lib/pattern_query_helper.rb | 35 ++++ lib/pattern_query_helper/column_map.rb | 37 ++++ lib/pattern_query_helper/filter.rb | 111 ++++++++++++ lib/pattern_query_helper/query_filter.rb | 50 ++++++ spec/pattern_query_helper/column_map_spec.rb | 26 +++ spec/pattern_query_helper/filter_spec.rb | 176 +++++++++++++++++++ spec/spec_helper.rb | 2 + 8 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 lib/pattern_query_helper/column_map.rb create mode 100644 lib/pattern_query_helper/filter.rb create mode 100644 lib/pattern_query_helper/query_filter.rb create mode 100644 spec/pattern_query_helper/column_map_spec.rb create mode 100644 spec/pattern_query_helper/filter_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 0d476d6..84bcaee 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - pattern_query_helper (0.2.9) + pattern_query_helper (0.2.10) activerecord (~> 5.0) kaminari (~> 1.1.1) diff --git a/lib/pattern_query_helper.rb b/lib/pattern_query_helper.rb index 8c464f5..f8b1200 100644 --- a/lib/pattern_query_helper.rb +++ b/lib/pattern_query_helper.rb @@ -5,8 +5,43 @@ require "pattern_query_helper/sorting" require "pattern_query_helper/sql" + + +require "pattern_query_helper/filter" +require "pattern_query_helper/column_map" + module PatternQueryHelper + def initialize( + type:, # :active_record or :sql + query:, # the query to be executed (either an active record query or a sql query) + query_params: {}, # hash of variables to bind into the query + page: nil, + per_page: nil, + filter: nil, + column_map:, + sort: nil, + include: nil, + associations: nil, + as_json: nil, + valid_columns: [], + single_record: false + ) + @type = type + @query = query + @query_params = query_params + @page = page + @per_page = per_page + @filter = filter + @sort = sort + @include = include + @associations = associations + @as_json = as_json + @valid_columns = valid_columns + @single_record = single_record + + end + def self.run_sql_query(model, query, query_params, query_helpers, valid_columns=[], single_record=false) if single_record single_record_sql_query(model, query, query_params, query_helpers, valid_columns) diff --git a/lib/pattern_query_helper/column_map.rb b/lib/pattern_query_helper/column_map.rb new file mode 100644 index 0000000..b4d7023 --- /dev/null +++ b/lib/pattern_query_helper/column_map.rb @@ -0,0 +1,37 @@ +module PatternQueryHelper + class ColumnMap + + attr_accessor :alias_name, :sql_expression, :aggregate + + def initialize( + alias_name:, + sql_expression:, + aggregate: false + ) + @alias_name = alias_name + @sql_expression = sql_expression + @aggregate = aggregate + end + + def self.create_from_hash(hash) + map = [] + hash.each do |k,v| + alias_name = k + aggregate = false + if v.class == String + sql_expression = v + elsif v.class == Hash + sql_expression = v[:sql_expression] + aggregate = v[:aggregate] + end + map << self.new( + alias_name: alias_name, + sql_expression: sql_expression, + aggregate: aggregate + ) + end + map + end + + end +end diff --git a/lib/pattern_query_helper/filter.rb b/lib/pattern_query_helper/filter.rb new file mode 100644 index 0000000..8b494a2 --- /dev/null +++ b/lib/pattern_query_helper/filter.rb @@ -0,0 +1,111 @@ +module PatternQueryHelper + class Filter + + attr_accessor :operator, :criterion, :comparate, :operator_code, :aggregate, :bind_variable, :cte_filter + + def initialize( + operator_code:, + criterion:, + comparate:, + aggregate: false, + cte_filter: true # Filter after creating a common table expression with the rest of the query. This will happen if the filter map doesn't include the comparate requested. + ) + @operator_code = operator_code + @criterion = criterion # Converts to a string to be inserted into sql. + @comparate = comparate + @aggregate = aggregate + @bind_variable = SecureRandom.hex.to_sym + + translate_operator_code() + mofify_criterion() + modify_comparate() + validate_criterion() + end + + def sql_string + case operator_code + when "in", "notin" + "#{comparate} #{operator} (:#{bind_variable})" + when "null" + "#{comparate} #{operator}" + else + "#{comparate} #{operator} :#{bind_variable}" + end + + end + + private + + def translate_operator_code + @operator = case operator_code + when "gte" + ">=" + when "lte" + "<=" + when "gt" + ">" + when "lt" + "<" + when "eql" + "=" + when "noteql" + "!=" + when "in" + "in" + when "like" + "like" + when "notin" + "not in" + when "null" + if criterion.to_s == "true" + "is null" + else + "is not null" + end + else + raise ArgumentError.new("Invalid operator code: '#{operator_code}'") + end + end + + def mofify_criterion + # lowercase strings for comparison + @criterion.downcase! if criterion.class == String && criterion.scan(/[a-zA-Z]/).any? + + # turn the criterion into an array for in and notin comparisons + @criterion = criterion.split(",") if ["in", "notin"].include?(operator_code) && criterion.class != Array + end + + def modify_comparate + # lowercase strings for comparison + @comparate = "lower(#{@comparate})" if criterion.class == String && criterion.scan(/[a-zA-Z]/).any? + end + + def validate_criterion + case operator_code + when "gte", "lte", "gt", "lt" + begin + Time.parse(criterion.to_s) + rescue + begin + Date.parse(criterion.to_s) + rescue + begin + Float(criterion.to_s) + rescue + invalid_criterion_error() + end + end + end + when "in", "notin" + invalid_criterion_error() unless criterion.class == Array + when "null" + invalid_criterion_error() unless ["true", "false"].include?(criterion.to_s) + end + true + end + + def invalid_criterion_error + raise ArgumentError.new("'#{criterion}' is not a valid criterion for the '#{operator}' operator") + end + end +end diff --git a/lib/pattern_query_helper/query_filter.rb b/lib/pattern_query_helper/query_filter.rb new file mode 100644 index 0000000..27e6902 --- /dev/null +++ b/lib/pattern_query_helper/query_filter.rb @@ -0,0 +1,50 @@ +module PatternQueryHelper + class QueryFilter + + attr_accessor :column_maps, :filter_values, :filters, :sql_string, :cte_filter, :embedded_having_strings, :embedded_where_strings, :bind_variables + + def initialize( + filter_values:, + column_maps: + ) + @column_maps = column_maps + @filter_values = filter_values + @filters = create_filters() + @cte_strings = filters.select{ |f| f.cte_filter == true }.map(&:sql_string) + @embedded_having_strings = filters.select{ |f| f.cte_filter == false && aggregate == true }.map(&:sql_string) + @embedded_where_strings = filters.select{ |f| f.cte_filter == false && aggregate == false }.map(&:sql_string) + @bind_variables = Hash[filters.collect { |f| [f.bind_variable, f.criterion] }] + end + + def create_filters + filters = [] + filter_values.each do |comparate, criteria| + # Default values + aggregate = false + cte_filter = true + + # Find the sql mapping if it exists + map = column_maps.find{ |m| m.alias_name == comparate } # Find the sql mapping if it exists + if map + comparate = map.sql_expression + aggregate = map.aggregate + cte_filter = false + end + + # Set the criteria + operator_code = criteria.keys.first + criterion = criteria.values.first + + # create the filter + filters << PatternQueryHelper::Filter.new( + operator_code: operator_code, + criterion: criterion, + comparate: comparate, + aggregate: aggregate, + cte_filter: cte_filter + ) + end + filters + end + end +end diff --git a/spec/pattern_query_helper/column_map_spec.rb b/spec/pattern_query_helper/column_map_spec.rb new file mode 100644 index 0000000..ce1d01c --- /dev/null +++ b/spec/pattern_query_helper/column_map_spec.rb @@ -0,0 +1,26 @@ +require "spec_helper" + +RSpec.describe PatternQueryHelper::ColumnMap do + let(:valid_operator_codes) {["gte", "lte", "gt", "lt", "eql", "noteql", "in", "notin", "null"]} + + describe ".create_from_hash" do + let(:hash) do + { + "column1" => "table.column1", + "column2" => "table.column2", + "column3" => {sql_expression: "sum(table.column3)", aggregate: true}, + "column4" => {sql_expression: "sum(table.column4)", aggregate: true}, + } + end + + it "creates an array of column maps" do + map = described_class.create_from_hash(hash) + expect(map.length).to eq(hash.keys.length) + expect(map.first.alias_name).to eq(hash.keys.first) + expect(map.first.sql_expression).to eq(hash.values.first) + expect(map.last.alias_name).to eq(hash.keys.last) + expect(map.last.sql_expression).to eq(hash.values.last[:sql_expression]) + expect(map.last.aggregate).to be true + end + end +end diff --git a/spec/pattern_query_helper/filter_spec.rb b/spec/pattern_query_helper/filter_spec.rb new file mode 100644 index 0000000..5d6f175 --- /dev/null +++ b/spec/pattern_query_helper/filter_spec.rb @@ -0,0 +1,176 @@ +require "spec_helper" + +RSpec.describe PatternQueryHelper::Filter do + let(:valid_operator_codes) {["gte", "lte", "gt", "lt", "eql", "noteql", "in", "notin", "null"]} + + describe ".sql_string" do + let(:filter) do + described_class.new( + operator_code: "gte", + criterion: Time.now, + column: "children.age" + ) + end + + it "creates sql string" do + sql_string = filter.sql_string() + byebug + expect(sql_string).to eq("#{filter.column} #{filter.operator} #{filter.criterion}") + end + + it "creates array correctly for in/not in" + it "lowercases text correctly" + it "creates sql_string correctly for null/not null comparisions" + end + + describe ".translate_operator_code" do + + # TODO: fix - Fails because criterion fails depending on the operator + # context "valid operator codes" do + # it "translates operator code correctly" do + # valid_operator_codes.each do |code| + # filter = described_class.new( + # operator_code: code, + # criterion: Faker::Number.between(0, 100), + # column: "children.age" + # ) + # expect(filter.operator).to_not be_nil + # end + # end + # end + + context "invalid operator code" do + it "raises an ArugmentError" do + expect{ + described_class.new( + operator_code: "fake_code", + criterion: Faker::Number.between(0, 100), + column: "children.age" + ) + }.to raise_error(ArgumentError) + end + end + end + + describe ".validate_criterion" do + RSpec.shared_examples "validates criterion" do + it "validates criterion" do + expect(filter.send(:validate_criterion)).to be true + end + end + + RSpec.shared_examples "invalidates criterion" do + it "validates criterion" do + expect{filter.send(:validate_criterion)}.to raise_error(ArgumentError) + end + end + + context "valid numeric criterion (gte, lte, gt, lt)" do + include_examples "validates criterion" + + let(:filter) do + described_class.new( + operator_code: "gte", + criterion: Faker::Number.between(0, 100), + column: "children.age" + ) + end + end + + context "valid date criterion (gte, lte, gt, lt)" do + include_examples "validates criterion" + + let(:filter) do + described_class.new( + operator_code: "gte", + criterion: Date.today, + column: "children.age" + ) + end + end + + context "valid time criterion (gte, lte, gt, lt)" do + include_examples "validates criterion" + + let(:filter) do + described_class.new( + operator_code: "gte", + criterion: Time.now, + column: "children.age" + ) + end + end + + context "invalid criterion (gte, lte, gt, lt)" do + include_examples "invalidates criterion" + + let(:filter) do + described_class.new( + operator_code: "gte", + criterion: "hello", + column: "children.age" + ) + end + end + + context "valid array criterion (in, notin)" do + include_examples "validates criterion" + + let(:filter) do + described_class.new( + operator_code: "in", + criterion: [1,2,3,4], + column: "children.age" + ) + end + end + + context "invalid criterion (in, notin)" do + include_examples "invalidates criterion" + + let(:filter) do + described_class.new( + operator_code: "in", + criterion: Date.today, + column: "children.age" + ) + end + end + + context "valid 'true' boolean criterion (null)" do + include_examples "validates criterion" + + let(:filter) do + described_class.new( + operator_code: "null", + criterion: true, + column: "children.age" + ) + end + end + + context "valid 'false' boolean criterion (null)" do + include_examples "validates criterion" + + let(:filter) do + described_class.new( + operator_code: "null", + criterion: false, + column: "children.age" + ) + end + end + + context "invalid boolean (null)" do + include_examples "invalidates criterion" + + let(:filter) do + described_class.new( + operator_code: "null", + criterion: "stringything", + column: "children.age" + ) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 45556db..ad8dc04 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -48,10 +48,12 @@ ActiveRecord::Schema.define do create_table :parents, force: true do |t| t.string :name + t.integer :age end create_table :children, force: true do |t| t.string :name t.references :parent + t.integer :age end end From 636d96d2725ef9af8bc3896a0bf33ba434fe8aa9 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Mon, 17 Jun 2019 15:46:55 -0600 Subject: [PATCH 02/48] Added query string class with ability to modify --- lib/pattern_query_helper.rb | 1 + lib/pattern_query_helper/query_string.rb | 78 +++++++++++++++ .../pattern_query_helper/query_string_spec.rb | 99 +++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 lib/pattern_query_helper/query_string.rb create mode 100644 spec/pattern_query_helper/query_string_spec.rb diff --git a/lib/pattern_query_helper.rb b/lib/pattern_query_helper.rb index f8b1200..fbf2b3d 100644 --- a/lib/pattern_query_helper.rb +++ b/lib/pattern_query_helper.rb @@ -9,6 +9,7 @@ require "pattern_query_helper/filter" require "pattern_query_helper/column_map" +require "pattern_query_helper/query_string" module PatternQueryHelper diff --git a/lib/pattern_query_helper/query_string.rb b/lib/pattern_query_helper/query_string.rb new file mode 100644 index 0000000..8bfd52b --- /dev/null +++ b/lib/pattern_query_helper/query_string.rb @@ -0,0 +1,78 @@ +module PatternQueryHelper + class QueryString + + attr_accessor :query_string, :modified_query_string + + def initialize(query_string) + @query_string = query_string.squish + @modified_query_string = @query_string + calculate_indexes() + true + + end + + def calculate_indexes + @last_select_index = modified_query_string.rindex(/[Ss][Ee][Ll][Ee][Cc][Tt]/) + @last_where_index = modified_query_string.index(/[Ww][Hh][Ee][Rr][Ee]/, @last_select_index) + @last_group_by_index = modified_query_string.index(/[Gg][Rr][Oo][Uu][Pp] [Bb][Yy]/, @last_select_index) + @last_having_index = modified_query_string.index(/[Hh][Aa][Vv][Ii][Nn][Gg]/, @last_select_index) + @last_order_by_index = modified_query_string.index(/[Oo][Rr][Dd][Ee][Rr] [Bb][Yy]/, @last_select_index) + + @where_included = !@last_where_index.nil? + @group_by_included = !@last_group_by_index.nil? + @having_included = !@last_having_index.nil? + @order_by_included = !@last_order_by_index.nil? + + @insert_where_index = @last_group_by_index || @last_order_by_index || @modified_query_string.length + @insert_having_index = @last_order_by_index || @modified_query_string.length + @insert_order_by_index = @modified_query_string.length + end + + def add_where_filters(filters) + begin_string = @where_included ? "and" : "where" + filter_string = filters.join(" and ") + where_string = " #{begin_string} #{filter_string} " #included extra spaces at beginning and end to buffer insert with correct spacing + @modified_query_string.insert(@insert_where_index, where_string).squish! + calculate_indexes() # recalculate indexes now that the query has been modified + end + + def add_having_filters(filters) + raise ArgumentError.new("Cannot include a having filter unless there is a group by clause in the query") unless @group_by_included + begin_string = @having_included ? "and" : "having" + filter_string = filters.join(" and ") + having_string = " #{begin_string} #{filter_string} " #included extra spaces at beginning and end to buffer insert with correct spacing + @modified_query_string.insert(@insert_having_index, having_string).squish! + calculate_indexes() # recalculate indexes now that the query has been modified + end + + def add_sorting(sorts) + @modified_query_string = @modified_query_string.slice(0, @last_order_by_index) if @order_by_included # Remove previous sorting if it exists + calculate_indexes() + sort_string = " order by #{sorts.join(", ")} " # included extra spaces at beginning and end to buffer insert with correct spacing + @modified_query_string.insert(@insert_order_by_index, sort_string).squish! + calculate_indexes() # recalculate indexes now that the query has been modified + end + + def add_pagination(page, per_page) + raise ArgumentError.new("page and per_page must be integers") unless page.class == Integer && per_page.class == Integer + limit = per_page + offset = (page - 1) * per_page + pagination_string = " limit #{limit} offset #{offset} " + @modified_query_string.insert(@modified_query_string.length, pagination_string).squish! + end + + def modify_query( + where_filters: nil, + having_filters: nil, + sorts: nil, + page: nil, + per_page: nil + ) + add_pagination(page, per_page) if page && per_page + add_having_filters(having_filters) if having_filters + add_where_filters(where_filters) if where_filters + add_sorting(sort) if sorts + end + + end +end diff --git a/spec/pattern_query_helper/query_string_spec.rb b/spec/pattern_query_helper/query_string_spec.rb new file mode 100644 index 0000000..24f0faf --- /dev/null +++ b/spec/pattern_query_helper/query_string_spec.rb @@ -0,0 +1,99 @@ +require "spec_helper" + +RSpec.describe PatternQueryHelper::QueryString do + let(:complex_query) do + query = %{ + with cte as ( + select * from table1 + ), cte1 as ( + select column1, column2 from table2 + ) + select a, b, c, d, sum(e) + from table1 + join cte on cte.a = table1.a + where string = string + group by a,b,c,d + having sum(e) > 1 + order by random_column + } + described_class.new(query) + end + + let(:simple_query) do + query = "select a from b" + described_class.new(query) + end + + let(:simple_group_by_query) do + query = "select sum(a) from b group by c" + described_class.new(query) + end + + describe ".add_where_filters" do + let(:where_filters) {["a = a", "b = b", "c = c"]} + context "simple query" do + it "correctly sets indexes" do + simple_query.add_where_filters(where_filters) + expected_result = simple_query.modified_query_string.include?("where #{where_filters.join(" and ")}") + expect(expected_result).to be true + end + end + context "complex query" do + it "correctly sets indexes" do + complex_query.add_where_filters(where_filters) + expected_result = complex_query.modified_query_string.include?("where string = string and #{where_filters.join(" and ")}") + expect(expected_result).to be true + end + end + end + + describe ".add_sorting" do + let(:sorts) {["a desc", "b asc"]} + context "simple query" do + it "correctly adds custom sorting" do + simple_query.add_sorting(sorts) + expected_result = simple_query.modified_query_string.include?("order by #{sorts.join(", ")}") + expect(expected_result).to be true + end + end + context "complex query" do + it "correctly adds custom sorting" do + complex_query.add_sorting(sorts) + expected_result = complex_query.modified_query_string.include?("order by #{sorts.join(", ")}") + expect(expected_result).to be true + end + end + end + + describe ".add_having_filters" do + let(:having_filters) {["count(a) > 0", "sum(b) < 100"]} + context "simple query" do + it "raises error when no group by clause" do + expect{simple_query.add_having_filters(having_filters)}.to raise_error(ArgumentError) + end + end + context "simple group by query" do + it "correctly sets additional having filters" do + simple_group_by_query.add_having_filters(having_filters) + expected_result = simple_group_by_query.modified_query_string.include?("having #{having_filters.join(" and ")}") + expect(expected_result).to be true + end + end + context "complex query" do + it "correctly sets additional having filters" do + complex_query.add_having_filters(having_filters) + expected_result = complex_query.modified_query_string.include?("having sum(e) > 1 and #{having_filters.join(" and ")}") + expect(expected_result).to be true + end + end + end + + describe ".add_pagination" do + it "adds correct pagination to query" do + complex_query.add_pagination(2,3) + expected_result = complex_query.modified_query_string.include?("limit 3 offset 3") + byebug + expect(expected_result).to be true + end + end +end From b2a62e9b84a8fee0b0601b1ba2cad1a1304e1ade Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Mon, 17 Jun 2019 15:51:20 -0600 Subject: [PATCH 03/48] removed byebug in test --- spec/pattern_query_helper/query_string_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/pattern_query_helper/query_string_spec.rb b/spec/pattern_query_helper/query_string_spec.rb index 24f0faf..c65f361 100644 --- a/spec/pattern_query_helper/query_string_spec.rb +++ b/spec/pattern_query_helper/query_string_spec.rb @@ -92,7 +92,6 @@ it "adds correct pagination to query" do complex_query.add_pagination(2,3) expected_result = complex_query.modified_query_string.include?("limit 3 offset 3") - byebug expect(expected_result).to be true end end From 48ce33e4080499b4a7c102970718ad9247a7fa81 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Mon, 17 Jun 2019 22:41:25 -0600 Subject: [PATCH 04/48] got query working --- lib/pattern_query_helper.rb | 2 + lib/pattern_query_helper/filter.rb | 7 +- lib/pattern_query_helper/query_filter.rb | 19 ++--- lib/pattern_query_helper/query_string.rb | 85 ++++++++++----------- lib/pattern_query_helper/sql_query.rb | 42 ++++++++++ spec/pattern_query_helper/sql_query_spec.rb | 37 +++++++++ spec/spec_helper.rb | 4 +- 7 files changed, 133 insertions(+), 63 deletions(-) create mode 100644 lib/pattern_query_helper/sql_query.rb create mode 100644 spec/pattern_query_helper/sql_query_spec.rb diff --git a/lib/pattern_query_helper.rb b/lib/pattern_query_helper.rb index fbf2b3d..2355071 100644 --- a/lib/pattern_query_helper.rb +++ b/lib/pattern_query_helper.rb @@ -10,6 +10,8 @@ require "pattern_query_helper/filter" require "pattern_query_helper/column_map" require "pattern_query_helper/query_string" +require "pattern_query_helper/sql_query" +require "pattern_query_helper/query_filter" module PatternQueryHelper diff --git a/lib/pattern_query_helper/filter.rb b/lib/pattern_query_helper/filter.rb index 8b494a2..7c65075 100644 --- a/lib/pattern_query_helper/filter.rb +++ b/lib/pattern_query_helper/filter.rb @@ -1,20 +1,19 @@ module PatternQueryHelper class Filter - attr_accessor :operator, :criterion, :comparate, :operator_code, :aggregate, :bind_variable, :cte_filter + attr_accessor :operator, :criterion, :comparate, :operator_code, :aggregate, :bind_variable def initialize( operator_code:, criterion:, comparate:, - aggregate: false, - cte_filter: true # Filter after creating a common table expression with the rest of the query. This will happen if the filter map doesn't include the comparate requested. + aggregate: false ) @operator_code = operator_code @criterion = criterion # Converts to a string to be inserted into sql. @comparate = comparate @aggregate = aggregate - @bind_variable = SecureRandom.hex.to_sym + @bind_variable = ('a'..'z').to_a.shuffle[0,20].join.to_sym translate_operator_code() mofify_criterion() diff --git a/lib/pattern_query_helper/query_filter.rb b/lib/pattern_query_helper/query_filter.rb index 27e6902..a728abe 100644 --- a/lib/pattern_query_helper/query_filter.rb +++ b/lib/pattern_query_helper/query_filter.rb @@ -1,34 +1,28 @@ module PatternQueryHelper class QueryFilter - attr_accessor :column_maps, :filter_values, :filters, :sql_string, :cte_filter, :embedded_having_strings, :embedded_where_strings, :bind_variables + attr_accessor :filters, :where_filter_strings, :having_filter_strings, :bind_variables - def initialize( - filter_values:, - column_maps: - ) + def initialize(filter_values:, column_maps:) @column_maps = column_maps @filter_values = filter_values @filters = create_filters() - @cte_strings = filters.select{ |f| f.cte_filter == true }.map(&:sql_string) - @embedded_having_strings = filters.select{ |f| f.cte_filter == false && aggregate == true }.map(&:sql_string) - @embedded_where_strings = filters.select{ |f| f.cte_filter == false && aggregate == false }.map(&:sql_string) + @where_filter_strings = filters.select{ |f| f.aggregate == false }.map(&:sql_string) + @having_filter_strings = filters.select{ |f| f.aggregate == true }.map(&:sql_string) @bind_variables = Hash[filters.collect { |f| [f.bind_variable, f.criterion] }] end def create_filters filters = [] - filter_values.each do |comparate, criteria| + @filter_values.each do |comparate, criteria| # Default values aggregate = false - cte_filter = true # Find the sql mapping if it exists - map = column_maps.find{ |m| m.alias_name == comparate } # Find the sql mapping if it exists + map = @column_maps.find{ |m| m.alias_name == comparate } # Find the sql mapping if it exists if map comparate = map.sql_expression aggregate = map.aggregate - cte_filter = false end # Set the criteria @@ -41,7 +35,6 @@ def create_filters criterion: criterion, comparate: comparate, aggregate: aggregate, - cte_filter: cte_filter ) end filters diff --git a/lib/pattern_query_helper/query_string.rb b/lib/pattern_query_helper/query_string.rb index 8bfd52b..42e1408 100644 --- a/lib/pattern_query_helper/query_string.rb +++ b/lib/pattern_query_helper/query_string.rb @@ -1,78 +1,75 @@ module PatternQueryHelper class QueryString - attr_accessor :query_string, :modified_query_string + attr_accessor :query_string, :where_filters, :having_filters, :sorts, :page, :per_page - def initialize(query_string) - @query_string = query_string.squish - @modified_query_string = @query_string + def initialize( + sql:, + where_filters: [], + having_filters: [], + sorts: [], + page: nil, + per_page: nil + ) + @sql = sql.squish + @where_filters = where_filters + @having_filters = having_filters + @sorts = sorts + @page = page + @per_page = per_page calculate_indexes() true - end def calculate_indexes - @last_select_index = modified_query_string.rindex(/[Ss][Ee][Ll][Ee][Cc][Tt]/) - @last_where_index = modified_query_string.index(/[Ww][Hh][Ee][Rr][Ee]/, @last_select_index) - @last_group_by_index = modified_query_string.index(/[Gg][Rr][Oo][Uu][Pp] [Bb][Yy]/, @last_select_index) - @last_having_index = modified_query_string.index(/[Hh][Aa][Vv][Ii][Nn][Gg]/, @last_select_index) - @last_order_by_index = modified_query_string.index(/[Oo][Rr][Dd][Ee][Rr] [Bb][Yy]/, @last_select_index) + @last_select_index = @sql.rindex(/[Ss][Ee][Ll][Ee][Cc][Tt]/) + @last_where_index = @sql.index(/[Ww][Hh][Ee][Rr][Ee]/, @last_select_index) + @last_group_by_index = @sql.index(/[Gg][Rr][Oo][Uu][Pp] [Bb][Yy]/, @last_select_index) + @last_having_index = @sql.index(/[Hh][Aa][Vv][Ii][Nn][Gg]/, @last_select_index) + @last_order_by_index = @sql.index(/[Oo][Rr][Dd][Ee][Rr] [Bb][Yy]/, @last_select_index) @where_included = !@last_where_index.nil? @group_by_included = !@last_group_by_index.nil? @having_included = !@last_having_index.nil? @order_by_included = !@last_order_by_index.nil? - @insert_where_index = @last_group_by_index || @last_order_by_index || @modified_query_string.length - @insert_having_index = @last_order_by_index || @modified_query_string.length - @insert_order_by_index = @modified_query_string.length + @insert_where_index = @last_group_by_index || @last_order_by_index || @sql.length + @insert_having_index = @last_order_by_index || @sql.length + @insert_order_by_index = @sql.length end - def add_where_filters(filters) + def where_insert begin_string = @where_included ? "and" : "where" - filter_string = filters.join(" and ") - where_string = " #{begin_string} #{filter_string} " #included extra spaces at beginning and end to buffer insert with correct spacing - @modified_query_string.insert(@insert_where_index, where_string).squish! - calculate_indexes() # recalculate indexes now that the query has been modified + filter_string = @where_filters.join(" and ") + " #{begin_string} #{filter_string} " end - def add_having_filters(filters) + def having_insert raise ArgumentError.new("Cannot include a having filter unless there is a group by clause in the query") unless @group_by_included begin_string = @having_included ? "and" : "having" - filter_string = filters.join(" and ") - having_string = " #{begin_string} #{filter_string} " #included extra spaces at beginning and end to buffer insert with correct spacing - @modified_query_string.insert(@insert_having_index, having_string).squish! - calculate_indexes() # recalculate indexes now that the query has been modified + filter_string = @having_filters.join(" and ") + " #{begin_string} #{filter_string} " end - def add_sorting(sorts) - @modified_query_string = @modified_query_string.slice(0, @last_order_by_index) if @order_by_included # Remove previous sorting if it exists - calculate_indexes() - sort_string = " order by #{sorts.join(", ")} " # included extra spaces at beginning and end to buffer insert with correct spacing - @modified_query_string.insert(@insert_order_by_index, sort_string).squish! - calculate_indexes() # recalculate indexes now that the query has been modified + def sort_insert + " order by #{sorts.join(", ")} " end - def add_pagination(page, per_page) + def pagination_insert raise ArgumentError.new("page and per_page must be integers") unless page.class == Integer && per_page.class == Integer limit = per_page offset = (page - 1) * per_page - pagination_string = " limit #{limit} offset #{offset} " - @modified_query_string.insert(@modified_query_string.length, pagination_string).squish! + " limit #{limit} offset #{offset} " end - def modify_query( - where_filters: nil, - having_filters: nil, - sorts: nil, - page: nil, - per_page: nil - ) - add_pagination(page, per_page) if page && per_page - add_having_filters(having_filters) if having_filters - add_where_filters(where_filters) if where_filters - add_sorting(sort) if sorts + def build + modified_sql = @sql.dup + modified_sql = modified_sql.slice(0, @last_order_by_index) if @order_by_included # Remove previous sorting if it exists + modified_sql.insert(modified_sql.length, pagination_insert) + modified_sql.insert(@insert_order_by_index, sort_insert) if @sorts && @sorts.length > 0 + modified_sql.insert(@insert_having_index, having_insert) if @having_filters && @having_filters.length > 0 + modified_sql.insert(@insert_where_index, where_insert) if @where_filters && @where_filters.length > 0 + modified_sql.squish end - end end diff --git a/lib/pattern_query_helper/sql_query.rb b/lib/pattern_query_helper/sql_query.rb new file mode 100644 index 0000000..c4b03cf --- /dev/null +++ b/lib/pattern_query_helper/sql_query.rb @@ -0,0 +1,42 @@ +module PatternQueryHelper + class SqlQuery + + attr_accessor :query_string, :query_params, :query_filter + + def initialize( + model:, # the model to run the query against + query:, # the custom sql to be executed + query_params: {}, + column_mappings: nil, # A hash that translates aliases to sql expressions + filters: nil, + page: nil, + per_page: nil + ) + @model = model + @query_params = query_params + @page = page.to_i if page + @per_page = per_page.to_i if per_page + + + @column_maps = PatternQueryHelper::ColumnMap.create_from_hash(column_mappings) + @query_filter = PatternQueryHelper::QueryFilter.new(filter_values: filters, column_maps: @column_maps) + + @query_string = PatternQueryHelper::QueryString.new( + sql: query, + where_filters: @query_filter.where_filter_strings, + having_filters: @query_filter.having_filter_strings, + sorts: [], + page: @page, + per_page: @per_page + ) + + @query_params.merge!(@query_filter.bind_variables) + end + + def execute_query + results = @model.find_by_sql([@query_string.build(), @query_params]) + byebug + results.as_json + end + end +end diff --git a/spec/pattern_query_helper/sql_query_spec.rb b/spec/pattern_query_helper/sql_query_spec.rb new file mode 100644 index 0000000..6d5d591 --- /dev/null +++ b/spec/pattern_query_helper/sql_query_spec.rb @@ -0,0 +1,37 @@ +require "spec_helper" + +RSpec.describe PatternQueryHelper::SqlQuery do + let(:query) do + %{ + select parents.name, count(children.id) as children_count + from parents + join children on parents.id = children.parent_id + group by parents.id + } + end + + let(:column_mappings) do + { + "children_count" => {sql_expression: "count(children.id)", aggregate: true}, + "name" => "parents.name", + "age" => "parents.age" + } + end + + let(:filters) do + {"age"=>{"lt"=>100}, "children_count"=>{"gt"=>0}} + end + + it "first test" do + sql_query = described_class.new( + model: Parent, + query: query, + column_mappings: column_mappings, + filters: filters, + page: 1, + per_page: 5 + ) + results = sql_query.execute_query + expected_results = Parent.all.to_a.select{ |p| p.children.length > 1 && p.age < 100 } + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ad8dc04..c99cb60 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -70,9 +70,9 @@ class Child < ApplicationRecord # Load data into databases (0..99).each do - parent = Parent.create(name: Faker::Name.name) + parent = Parent.create(name: Faker::Name.name, age: Faker::Number.between(25, 55)) (0..Faker::Number.between(1, 5)).each do - Child.create(name: Faker::Name.name, parent: parent) + Child.create(name: Faker::Name.name, parent: parent, age: Faker::Number.between(1, 25)) end end end From f6a532225ae0e49f387a8bfb30f7d834f673ab98 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 18 Jun 2019 09:39:55 -0600 Subject: [PATCH 05/48] Add pagination --- lib/pattern_query_helper/query_string.rb | 16 ++++++-- lib/pattern_query_helper/sql_query.rb | 43 ++++++++++++++++++--- spec/pattern_query_helper/filter_spec.rb | 1 - spec/pattern_query_helper/sql_query_spec.rb | 3 +- 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/lib/pattern_query_helper/query_string.rb b/lib/pattern_query_helper/query_string.rb index 42e1408..d98cbff 100644 --- a/lib/pattern_query_helper/query_string.rb +++ b/lib/pattern_query_helper/query_string.rb @@ -23,6 +23,7 @@ def initialize( def calculate_indexes @last_select_index = @sql.rindex(/[Ss][Ee][Ll][Ee][Cc][Tt]/) + @last_from_index = @sql.rindex(/[Ff][Rr][Oo][Mm]/) @last_where_index = @sql.index(/[Ww][Hh][Ee][Rr][Ee]/, @last_select_index) @last_group_by_index = @sql.index(/[Gg][Rr][Oo][Uu][Pp] [Bb][Yy]/, @last_select_index) @last_having_index = @sql.index(/[Hh][Aa][Vv][Ii][Nn][Gg]/, @last_select_index) @@ -36,6 +37,8 @@ def calculate_indexes @insert_where_index = @last_group_by_index || @last_order_by_index || @sql.length @insert_having_index = @last_order_by_index || @sql.length @insert_order_by_index = @sql.length + @insert_join_index = @last_where_index || @last_group_by_index || @last_order_by_index || @sql.length + @insert_select_index = @last_from_index end def where_insert @@ -56,19 +59,24 @@ def sort_insert end def pagination_insert - raise ArgumentError.new("page and per_page must be integers") unless page.class == Integer && per_page.class == Integer - limit = per_page - offset = (page - 1) * per_page + raise ArgumentError.new("page and per_page must be integers") unless @page.class == Integer && @per_page.class == Integer + limit = @per_page + offset = (@page - 1) * @per_page " limit #{limit} offset #{offset} " end + def total_count_select_insert + " ,count(*) over () as _query_full_count " + end + def build modified_sql = @sql.dup modified_sql = modified_sql.slice(0, @last_order_by_index) if @order_by_included # Remove previous sorting if it exists - modified_sql.insert(modified_sql.length, pagination_insert) + modified_sql.insert(modified_sql.length, pagination_insert) if @page && @per_page modified_sql.insert(@insert_order_by_index, sort_insert) if @sorts && @sorts.length > 0 modified_sql.insert(@insert_having_index, having_insert) if @having_filters && @having_filters.length > 0 modified_sql.insert(@insert_where_index, where_insert) if @where_filters && @where_filters.length > 0 + modified_sql.insert(@insert_select_index, total_count_select_insert) if @page && @per_page modified_sql.squish end end diff --git a/lib/pattern_query_helper/sql_query.rb b/lib/pattern_query_helper/sql_query.rb index c4b03cf..97d25e7 100644 --- a/lib/pattern_query_helper/sql_query.rb +++ b/lib/pattern_query_helper/sql_query.rb @@ -1,7 +1,7 @@ module PatternQueryHelper class SqlQuery - attr_accessor :query_string, :query_params, :query_filter + attr_accessor :model, :query_string, :query_params, :query_filter, :results def initialize( model:, # the model to run the query against @@ -17,7 +17,6 @@ def initialize( @page = page.to_i if page @per_page = per_page.to_i if per_page - @column_maps = PatternQueryHelper::ColumnMap.create_from_hash(column_mappings) @query_filter = PatternQueryHelper::QueryFilter.new(filter_values: filters, column_maps: @column_maps) @@ -31,12 +30,46 @@ def initialize( ) @query_params.merge!(@query_filter.bind_variables) + + execute_query() end def execute_query - results = @model.find_by_sql([@query_string.build(), @query_params]) - byebug - results.as_json + @results = @model.find_by_sql([@query_string.build(), @query_params]).as_json + @count = @page && @per_page && results.length > 0? results.first["_query_full_count"] : results.length + clean_results() + end + + def clean_results + @results.map!{ |r| r.except("_query_full_count") } if @page && @per_page + end + + def pagination_results + total_pages = (@count/(@per_page.nonzero? || 1).to_f).ceil + next_page = @page + 1 if @page.between?(1, total_pages - 1) + previous_page = @page - 1 if @page.between?(2, total_pages) + first_page = @page == 1 + last_page = @page == total_pages + out_of_range = !@page.between?(1,total_pages) + + { + count: @count, + current_page: @page, + next_page: next_page, + previous_page: previous_page, + total_pages: total_pages, + per_page: @per_page, + first_page: first_page, + last_page: last_page, + out_of_range: out_of_range + } + end + + def payload + { + pagination: pagination_results(), + data: @results + } end end end diff --git a/spec/pattern_query_helper/filter_spec.rb b/spec/pattern_query_helper/filter_spec.rb index 5d6f175..aefe8a4 100644 --- a/spec/pattern_query_helper/filter_spec.rb +++ b/spec/pattern_query_helper/filter_spec.rb @@ -14,7 +14,6 @@ it "creates sql string" do sql_string = filter.sql_string() - byebug expect(sql_string).to eq("#{filter.column} #{filter.operator} #{filter.criterion}") end diff --git a/spec/pattern_query_helper/sql_query_spec.rb b/spec/pattern_query_helper/sql_query_spec.rb index 6d5d591..5ba5917 100644 --- a/spec/pattern_query_helper/sql_query_spec.rb +++ b/spec/pattern_query_helper/sql_query_spec.rb @@ -31,7 +31,8 @@ page: 1, per_page: 5 ) - results = sql_query.execute_query expected_results = Parent.all.to_a.select{ |p| p.children.length > 1 && p.age < 100 } + byebug + end end From efc0d96bbc3f314a0ef3d21a7ee6ab2f116a7787 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 18 Jun 2019 10:02:46 -0600 Subject: [PATCH 06/48] updated tests --- lib/pattern_query_helper/sql_query.rb | 25 ++++++++++++--------- spec/pattern_query_helper/sql_query_spec.rb | 9 +++++--- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/pattern_query_helper/sql_query.rb b/lib/pattern_query_helper/sql_query.rb index 97d25e7..0c5f7f1 100644 --- a/lib/pattern_query_helper/sql_query.rb +++ b/lib/pattern_query_helper/sql_query.rb @@ -7,19 +7,19 @@ def initialize( model:, # the model to run the query against query:, # the custom sql to be executed query_params: {}, - column_mappings: nil, # A hash that translates aliases to sql expressions - filters: nil, + column_mappings: {}, # A hash that translates aliases to sql expressions + filters: {}, page: nil, - per_page: nil + per_page: nil, + single_record: false ) @model = model @query_params = query_params @page = page.to_i if page @per_page = per_page.to_i if per_page - + @single_record = single_record @column_maps = PatternQueryHelper::ColumnMap.create_from_hash(column_mappings) @query_filter = PatternQueryHelper::QueryFilter.new(filter_values: filters, column_maps: @column_maps) - @query_string = PatternQueryHelper::QueryString.new( sql: query, where_filters: @query_filter.where_filter_strings, @@ -28,14 +28,13 @@ def initialize( page: @page, per_page: @per_page ) - @query_params.merge!(@query_filter.bind_variables) - execute_query() end def execute_query @results = @model.find_by_sql([@query_string.build(), @query_params]).as_json + @results = @results.first if @single_record @count = @page && @per_page && results.length > 0? results.first["_query_full_count"] : results.length clean_results() end @@ -66,10 +65,14 @@ def pagination_results end def payload - { - pagination: pagination_results(), - data: @results - } + if @page && @per_page + { + pagination: pagination_results(), + data: @results + } + else + { data: @results } + end end end end diff --git a/spec/pattern_query_helper/sql_query_spec.rb b/spec/pattern_query_helper/sql_query_spec.rb index 5ba5917..535dcd3 100644 --- a/spec/pattern_query_helper/sql_query_spec.rb +++ b/spec/pattern_query_helper/sql_query_spec.rb @@ -22,7 +22,7 @@ {"age"=>{"lt"=>100}, "children_count"=>{"gt"=>0}} end - it "first test" do + it "returns a payload" do sql_query = described_class.new( model: Parent, query: query, @@ -31,8 +31,11 @@ page: 1, per_page: 5 ) + results = sql_query.payload() expected_results = Parent.all.to_a.select{ |p| p.children.length > 1 && p.age < 100 } - byebug - + expect(results[:pagination][:count]).to eq(expected_results.length) + expect(results[:pagination][:per_page]).to eq(5) + expect(results[:pagination][:page]).to eq(1) + expect(results[:data]).to eq(5) end end From 5735d543792439cd6053f7e3b340c2f1bcfb44db Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 18 Jun 2019 10:28:47 -0600 Subject: [PATCH 07/48] Add sorting --- lib/pattern_query_helper.rb | 1 + lib/pattern_query_helper/sort.rb | 46 +++++++++++++++++++++ lib/pattern_query_helper/sql_query.rb | 4 +- spec/pattern_query_helper/sql_query_spec.rb | 11 ++--- 4 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 lib/pattern_query_helper/sort.rb diff --git a/lib/pattern_query_helper.rb b/lib/pattern_query_helper.rb index 2355071..cc66f27 100644 --- a/lib/pattern_query_helper.rb +++ b/lib/pattern_query_helper.rb @@ -12,6 +12,7 @@ require "pattern_query_helper/query_string" require "pattern_query_helper/sql_query" require "pattern_query_helper/query_filter" +require "pattern_query_helper/sort" module PatternQueryHelper diff --git a/lib/pattern_query_helper/sort.rb b/lib/pattern_query_helper/sort.rb new file mode 100644 index 0000000..194d920 --- /dev/null +++ b/lib/pattern_query_helper/sort.rb @@ -0,0 +1,46 @@ +module PatternQueryHelper + class Sort + + attr_accessor :sort_strings + + def initialize(sort_string:, column_maps:) + @sort_string = sort_string + @column_maps = column_maps + @sort_strings = [] + parse_sort_string() + end + + def parse_sort_string + sorts = @sort_string.split(",") + sorts.each_with_index do |sort, index| + sort_alias = sort.split(":")[0] + direction = sort.split(":")[1] + modifier = sort.split(":")[2] + + begin + sql_expression = @column_maps.find{ |m| m.alias_name == sort_alias }.sql_expression + rescue NoMethodError => e + raise ArgumentError.new("Sorting not allowed on column '#{sort_alias}'") + end + + if direction == "desc" + case PatternQueryHelper.active_record_adapter + when "sqlite3" + direction = "desc" + else + direction = "desc nulls last" + end + else + direction = "asc" + end + + case modifier + when "lowercase" + sql_expression = "lower(#{sql_expression})" + end + + @sort_strings << "#{sql_expression} #{direction}" + end + end + end +end diff --git a/lib/pattern_query_helper/sql_query.rb b/lib/pattern_query_helper/sql_query.rb index 0c5f7f1..bc26c11 100644 --- a/lib/pattern_query_helper/sql_query.rb +++ b/lib/pattern_query_helper/sql_query.rb @@ -9,6 +9,7 @@ def initialize( query_params: {}, column_mappings: {}, # A hash that translates aliases to sql expressions filters: {}, + sorts: "", page: nil, per_page: nil, single_record: false @@ -20,11 +21,12 @@ def initialize( @single_record = single_record @column_maps = PatternQueryHelper::ColumnMap.create_from_hash(column_mappings) @query_filter = PatternQueryHelper::QueryFilter.new(filter_values: filters, column_maps: @column_maps) + @sorts = PatternQueryHelper::Sort.new(sort_string: sorts, column_maps: @column_maps) @query_string = PatternQueryHelper::QueryString.new( sql: query, where_filters: @query_filter.where_filter_strings, having_filters: @query_filter.having_filter_strings, - sorts: [], + sorts: @sorts.sort_strings, page: @page, per_page: @per_page ) diff --git a/spec/pattern_query_helper/sql_query_spec.rb b/spec/pattern_query_helper/sql_query_spec.rb index 535dcd3..0724098 100644 --- a/spec/pattern_query_helper/sql_query_spec.rb +++ b/spec/pattern_query_helper/sql_query_spec.rb @@ -18,14 +18,15 @@ } end - let(:filters) do - {"age"=>{"lt"=>100}, "children_count"=>{"gt"=>0}} - end + let(:filters) { {"age"=>{"lt"=>100}, "children_count"=>{"gt"=>0}} } + + let(:sorts) {"name:asc:lowercase,age:desc"} it "returns a payload" do sql_query = described_class.new( model: Parent, query: query, + sorts: sorts, column_mappings: column_mappings, filters: filters, page: 1, @@ -35,7 +36,7 @@ expected_results = Parent.all.to_a.select{ |p| p.children.length > 1 && p.age < 100 } expect(results[:pagination][:count]).to eq(expected_results.length) expect(results[:pagination][:per_page]).to eq(5) - expect(results[:pagination][:page]).to eq(1) - expect(results[:data]).to eq(5) + expect(results[:pagination][:current_page]).to eq(1) + expect(results[:data].length).to eq(5) end end From 6353ab7312886128622e2faf4a5fdeabc4227df1 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 18 Jun 2019 13:23:54 -0600 Subject: [PATCH 08/48] load associations --- lib/pattern_query_helper/associations.rb | 2 +- lib/pattern_query_helper/sql_query.rb | 25 ++++++++++++++++++--- spec/pattern_query_helper/sql_query_spec.rb | 8 ++++++- spec/spec_helper.rb | 4 ++++ 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/lib/pattern_query_helper/associations.rb b/lib/pattern_query_helper/associations.rb index f2e8bc7..887fe54 100644 --- a/lib/pattern_query_helper/associations.rb +++ b/lib/pattern_query_helper/associations.rb @@ -5,7 +5,7 @@ def self.process_association_params(associations) associations.class == String ? [associations.to_sym] : associations end - def self.load_associations(payload, associations, as_json_options) + def self.load_associations(payload:, associations:, as_json_options:) as_json_options ||= {} as_json_options[:include] = as_json_options[:include] || json_associations(associations) ActiveRecord::Associations::Preloader.new.preload(payload, associations) diff --git a/lib/pattern_query_helper/sql_query.rb b/lib/pattern_query_helper/sql_query.rb index bc26c11..a1b3172 100644 --- a/lib/pattern_query_helper/sql_query.rb +++ b/lib/pattern_query_helper/sql_query.rb @@ -12,16 +12,20 @@ def initialize( sorts: "", page: nil, per_page: nil, - single_record: false + single_record: false, + associations: [], + as_json_options: {} ) @model = model @query_params = query_params @page = page.to_i if page @per_page = per_page.to_i if per_page @single_record = single_record + @as_json_options = as_json_options @column_maps = PatternQueryHelper::ColumnMap.create_from_hash(column_mappings) @query_filter = PatternQueryHelper::QueryFilter.new(filter_values: filters, column_maps: @column_maps) @sorts = PatternQueryHelper::Sort.new(sort_string: sorts, column_maps: @column_maps) + @associations = PatternQueryHelper::Associations.process_association_params(associations) @query_string = PatternQueryHelper::QueryString.new( sql: query, where_filters: @query_filter.where_filter_strings, @@ -35,12 +39,27 @@ def initialize( end def execute_query - @results = @model.find_by_sql([@query_string.build(), @query_params]).as_json - @results = @results.first if @single_record + # Execute Sql Query + @results = @model.find_by_sql([@query_string.build(), @query_params]) + + # Determine total result count @count = @page && @per_page && results.length > 0? results.first["_query_full_count"] : results.length + + # Return a single result if requested + @results = @results.first if @single_record + + load_associations() clean_results() end + def load_associations + @results = PatternQueryHelper::Associations.load_associations( + payload: @results, + associations: @associations, + as_json_options: @as_json_options + ) + end + def clean_results @results.map!{ |r| r.except("_query_full_count") } if @page && @per_page end diff --git a/spec/pattern_query_helper/sql_query_spec.rb b/spec/pattern_query_helper/sql_query_spec.rb index 0724098..c5d8753 100644 --- a/spec/pattern_query_helper/sql_query_spec.rb +++ b/spec/pattern_query_helper/sql_query_spec.rb @@ -3,7 +3,7 @@ RSpec.describe PatternQueryHelper::SqlQuery do let(:query) do %{ - select parents.name, count(children.id) as children_count + select parents.id, parents.name, count(children.id) as children_count from parents join children on parents.id = children.parent_id group by parents.id @@ -22,6 +22,10 @@ let(:sorts) {"name:asc:lowercase,age:desc"} + let(:includes) {"children"} + + let(:as_json_options) {{ methods: [:favorite_star_wars_character] }} + it "returns a payload" do sql_query = described_class.new( model: Parent, @@ -29,6 +33,8 @@ sorts: sorts, column_mappings: column_mappings, filters: filters, + associations: includes, + as_json_options: as_json_options, page: 1, per_page: 5 ) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c99cb60..3a13acc 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -63,6 +63,10 @@ class ApplicationRecord < ActiveRecord::Base end class Parent < ApplicationRecord has_many :children + + def favorite_star_wars_character + Faker::Movies::StarWars.character + end end class Child < ApplicationRecord belongs_to :parent From 877070283c3428d61bc16a1ffc552a6ebfb9f95e Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 18 Jun 2019 14:03:05 -0600 Subject: [PATCH 09/48] Removed old files, fixed some tests --- lib/pattern_query_helper.rb | 141 +----------------- lib/pattern_query_helper/associations.rb | 2 +- lib/pattern_query_helper/filter.rb | 2 +- lib/pattern_query_helper/filtering.rb | 84 ----------- lib/pattern_query_helper/pagination.rb | 47 ------ lib/pattern_query_helper/sorting.rb | 40 ----- lib/pattern_query_helper/sql.rb | 57 ------- lib/pattern_query_helper/url_helper.rb | 21 +++ .../pattern_query_helper/associations_spec.rb | 2 +- spec/pattern_query_helper/filter_spec.rb | 26 ++-- spec/pattern_query_helper/filtering_spec.rb | 48 ------ spec/pattern_query_helper/pagination_spec.rb | 73 --------- .../pattern_query_helper/query_string_spec.rb | 67 +-------- spec/pattern_query_helper/sorting_spec.rb | 16 -- spec/pattern_query_helper/sql_spec.rb | 103 ------------- spec/pattern_query_helper_spec.rb | 38 +---- 16 files changed, 40 insertions(+), 727 deletions(-) delete mode 100644 lib/pattern_query_helper/filtering.rb delete mode 100644 lib/pattern_query_helper/pagination.rb delete mode 100644 lib/pattern_query_helper/sorting.rb delete mode 100644 lib/pattern_query_helper/sql.rb create mode 100644 lib/pattern_query_helper/url_helper.rb delete mode 100644 spec/pattern_query_helper/filtering_spec.rb delete mode 100644 spec/pattern_query_helper/pagination_spec.rb delete mode 100644 spec/pattern_query_helper/sorting_spec.rb delete mode 100644 spec/pattern_query_helper/sql_spec.rb diff --git a/lib/pattern_query_helper.rb b/lib/pattern_query_helper.rb index cc66f27..ce0bd1c 100644 --- a/lib/pattern_query_helper.rb +++ b/lib/pattern_query_helper.rb @@ -1,153 +1,14 @@ require "pattern_query_helper/version" -require "pattern_query_helper/pagination" -require "pattern_query_helper/filtering" -require "pattern_query_helper/associations" -require "pattern_query_helper/sorting" -require "pattern_query_helper/sql" - - - require "pattern_query_helper/filter" require "pattern_query_helper/column_map" require "pattern_query_helper/query_string" require "pattern_query_helper/sql_query" require "pattern_query_helper/query_filter" require "pattern_query_helper/sort" +require "pattern_query_helper/associations" module PatternQueryHelper - def initialize( - type:, # :active_record or :sql - query:, # the query to be executed (either an active record query or a sql query) - query_params: {}, # hash of variables to bind into the query - page: nil, - per_page: nil, - filter: nil, - column_map:, - sort: nil, - include: nil, - associations: nil, - as_json: nil, - valid_columns: [], - single_record: false - ) - @type = type - @query = query - @query_params = query_params - @page = page - @per_page = per_page - @filter = filter - @sort = sort - @include = include - @associations = associations - @as_json = as_json - @valid_columns = valid_columns - @single_record = single_record - - end - - def self.run_sql_query(model, query, query_params, query_helpers, valid_columns=[], single_record=false) - if single_record - single_record_sql_query(model, query, query_params, query_helpers, valid_columns) - elsif query_helpers[:per_page] || query_helpers[:page] - paginated_sql_query(model, query, query_params, query_helpers, valid_columns) - else - sql_query(model, query, query_params, query_helpers, valid_columns) - end - end - - def self.run_active_record_query(active_record_call, query_helpers, valid_columns=[], single_record=false) - run_sql_query(active_record_call.model, active_record_call.to_sql, {}, query_helpers, valid_columns, single_record) - end - - private - - def self.paginated_sql_query(model, query, query_params, query_helpers, valid_columns) - query_helpers = parse_helpers(query_helpers, valid_columns) - - query_config = { - model: model, - query: query, - query_params: query_params, - page: query_helpers[:pagination][:page], - per_page: query_helpers[:pagination][:per_page], - filter_string: query_helpers[:filters][:filter_string], - filter_params: query_helpers[:filters][:filter_params], - sort_string: query_helpers[:sorting], - } - - data = PatternQueryHelper::Sql.sql_query(query_config) - data = PatternQueryHelper::Associations.load_associations(data, query_helpers[:associations], query_helpers[:as_json]) - count = PatternQueryHelper::Sql.sql_query_count(data) - data.map! { |d| d.except(PatternQueryHelper::Sql::QUERY_COUNT_COLUMN) } if query_config[:page] or query_config[:per_page] - pagination = PatternQueryHelper::Pagination.create_pagination_payload(count, query_helpers[:pagination]) - - { - pagination: pagination, - data: data - } - end - - def self.sql_query(model, query, query_params, query_helpers, valid_columns) - query_helpers = parse_helpers(query_helpers, valid_columns) - - query_config = { - model: model, - query: query, - query_params: query_params, - filter_string: query_helpers[:filters][:filter_string], - filter_params: query_helpers[:filters][:filter_params], - sort_string: query_helpers[:sorting], - } - - data = PatternQueryHelper::Sql.sql_query(query_config) - data = PatternQueryHelper::Associations.load_associations(data, query_helpers[:associations], query_helpers[:as_json]) - - { - data: data - } - end - - def self.single_record_sql_query(model, query, query_params, query_helpers, valid_columns) - query_helpers = parse_helpers(query_helpers, valid_columns) - - query_config = { - model: model, - query: query, - query_params: query_params, - filter_string: query_helpers[:filters][:filter_string], - filter_params: query_helpers[:filters][:filter_params], - sort_string: query_helpers[:sorting], - } - - data = PatternQueryHelper::Sql.single_record_query(query_config) - data = PatternQueryHelper::Associations.load_associations(data, query_helpers[:associations], query_helpers[:as_json]) - - { - data: data - } - end - - def self.parse_helpers(query_helpers, valid_columns) - valid_columns_map = {} - valid_columns.each do |c| - valid_columns_map["#{c}"] = c - end - filtering = PatternQueryHelper::Filtering.create_filters(filters: query_helpers[:filter], valid_columns_map: valid_columns_map) - sorting = PatternQueryHelper::Sorting.parse_sorting_params(query_helpers[:sort], valid_columns) - associations = PatternQueryHelper::Associations.process_association_params(query_helpers[:include]) - pagination = PatternQueryHelper::Pagination.parse_pagination_params(query_helpers[:page], query_helpers[:per_page]) - as_json = query_helpers[:as_json] - - { - filters: filtering, - sorting: sorting, - associations: associations, - pagination: pagination, - as_json: as_json - } - end - class << self attr_accessor :active_record_adapter end diff --git a/lib/pattern_query_helper/associations.rb b/lib/pattern_query_helper/associations.rb index 887fe54..eddd26f 100644 --- a/lib/pattern_query_helper/associations.rb +++ b/lib/pattern_query_helper/associations.rb @@ -5,7 +5,7 @@ def self.process_association_params(associations) associations.class == String ? [associations.to_sym] : associations end - def self.load_associations(payload:, associations:, as_json_options:) + def self.load_associations(payload:, associations: [], as_json_options: {}) as_json_options ||= {} as_json_options[:include] = as_json_options[:include] || json_associations(associations) ActiveRecord::Associations::Preloader.new.preload(payload, associations) diff --git a/lib/pattern_query_helper/filter.rb b/lib/pattern_query_helper/filter.rb index 7c65075..8365d71 100644 --- a/lib/pattern_query_helper/filter.rb +++ b/lib/pattern_query_helper/filter.rb @@ -71,7 +71,7 @@ def mofify_criterion @criterion.downcase! if criterion.class == String && criterion.scan(/[a-zA-Z]/).any? # turn the criterion into an array for in and notin comparisons - @criterion = criterion.split(",") if ["in", "notin"].include?(operator_code) && criterion.class != Array + @criterion = criterion.split(",") if ["in", "notin"].include?(operator_code) && criterion.class == String end def modify_comparate diff --git a/lib/pattern_query_helper/filtering.rb b/lib/pattern_query_helper/filtering.rb deleted file mode 100644 index 51cda3a..0000000 --- a/lib/pattern_query_helper/filtering.rb +++ /dev/null @@ -1,84 +0,0 @@ -module PatternQueryHelper - class Filtering - def self.create_filters(filters:, valid_columns_map: nil, symbol_prefix: "", include_where: true) - filters ||= {} - all_conditions = [] - filter_params = {} - filter_array = [] - filters.each do |filter_attribute, criteria| - if valid_columns_map - raise ArgumentError.new("Invalid filter '#{filter_attribute}'") unless valid_columns_map[filter_attribute] - filter_column = valid_columns_map[filter_attribute] - else - filter_column = filter_attribute - end - criteria.each do |operator_code, criterion| - filter_symbol = "#{symbol_prefix}#{filter_attribute}_#{operator_code}" - case operator_code - when "gte" - operator = ">=" - when "lte" - operator = "<=" - when "gt" - operator = ">" - when "lt" - operator = "<" - when "eql" - operator = "=" - when "noteql" - operator = "!=" - when "like" - modified_filter_column = "lower(#{filter_column})" - operator = "like" - criterion.downcase! - when "in" - operator = "in (:#{filter_symbol})" - # if criterion are anything but numbers, downcase the filter_column - if criterion.scan(/[^\d|,|\s]/).any? - modified_filter_column = "lower(#{filter_column})" - end - criterion = criterion.downcase.split(",") - filter_symbol_already_embedded = true - when "notin" - operator = "not in (:#{filter_symbol})" - # if criterion are anything but numbers, downcase the filter_column - if criterion.scan(/[^\d|,|\s]/).any? - modified_filter_column = "lower(#{filter_column})" - end - criterion = criterion.downcase.split(",") - filter_symbol_already_embedded = true - when "null" - operator = criterion.to_s == "true" ? "is null" : "is not null" - filter_symbol = "" - else - raise ArgumentError.new("Invalid operator code '#{operator_code}' on '#{filter_attribute}' filter") - end - filter_column = modified_filter_column || filter_column - condition = "#{filter_column} #{operator}" - condition << " :#{filter_symbol}" unless filter_symbol_already_embedded or filter_symbol.blank? - all_conditions << condition - filter_params["#{filter_symbol}"] = criterion unless filter_symbol.blank? - filter_array << { - column: filter_attribute, - operator: operator, - value: criterion, - symbol: filter_symbol - } - end - end - - if include_where - filter_string = "where " + all_conditions.join("\n and ") unless all_conditions.empty? - else - filter_string = all_conditions.empty? ? "1 = 1" : all_conditions.join("\n and ") - end - - { - filter_string: filter_string, - filter_params: filter_params, - filter_array: filter_array - } - - end - end -end diff --git a/lib/pattern_query_helper/pagination.rb b/lib/pattern_query_helper/pagination.rb deleted file mode 100644 index 5bbd397..0000000 --- a/lib/pattern_query_helper/pagination.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'active_record' -require 'kaminari' - -module PatternQueryHelper - class Pagination - def self.parse_pagination_params(page, per_page) - page = page ? page.to_i : 1 - per_page = per_page ? per_page.to_i : 20 - raise RangeError.new("page must be greater than 0") unless page > 0 - raise RangeError.new("per_page must be greater than 0") unless per_page > 0 - - { - page: page, - per_page: per_page - } - end - - def self.create_pagination_payload(count, pagination_params) - page = pagination_params[:page] - per_page = pagination_params[:per_page] - total_pages = (count/(per_page.nonzero? || 1).to_f).ceil - next_page = page + 1 if page.between?(1, total_pages - 1) - previous_page = page - 1 if page.between?(2, total_pages) - first_page = page == 1 - last_page = page == total_pages - out_of_range = !page.between?(1,total_pages) - - { - count: count, - current_page: page, - next_page: next_page, - previous_page: previous_page, - total_pages: total_pages, - per_page: per_page, - first_page: first_page, - last_page: last_page, - out_of_range: out_of_range - } - end - - def self.paginate_active_record_query(active_record_call, pagination_params) - page = pagination_params[:page] - per_page = pagination_params[:per_page] - active_record_call.page(page).per(per_page) - end - end -end diff --git a/lib/pattern_query_helper/sorting.rb b/lib/pattern_query_helper/sorting.rb deleted file mode 100644 index 01ad449..0000000 --- a/lib/pattern_query_helper/sorting.rb +++ /dev/null @@ -1,40 +0,0 @@ -module PatternQueryHelper - class Sorting - def self.parse_sorting_params(sort, valid_columns) - sort_sql = [] - if sort - sorts = sort.split(",") - sorts.each_with_index do |sort, index| - column = sort.split(":")[0] - direction = sort.split(":")[1] - modifier = sort.split(":")[2] - - raise ArgumentError.new("Sorting not allowed on column '#{column}'") unless valid_columns.include? column - - if direction == "desc" - case PatternQueryHelper.active_record_adapter - when "sqlite3" - direction = "desc" - else - direction = "desc nulls last" - end - else - direction = "asc" - end - - case modifier - when "lowercase" - column = "lower(#{column})" - end - - sort_sql << "#{column} #{direction}" - end - end - sort_sql.join(", ") - end - - def self.sort_active_record_query(active_record_call, sort_string) - active_record_call.order(sort_string) - end - end -end diff --git a/lib/pattern_query_helper/sql.rb b/lib/pattern_query_helper/sql.rb deleted file mode 100644 index bda7300..0000000 --- a/lib/pattern_query_helper/sql.rb +++ /dev/null @@ -1,57 +0,0 @@ -module PatternQueryHelper - class Sql - QUERY_COUNT_COLUMN = "_query_full_count".freeze - def self.sql_query(config) - model = config[:model] - query_params = config[:query_params] || {} - page = config[:page] - per_page = config[:per_page] - filter_params = config[:filter_params] || {} - - if page && per_page - query_params[:limit] = per_page - query_params[:offset] = (page - 1) * per_page - limit = "limit :limit offset :offset" - end - - full_count_join = "cross join (select count(*) as #{QUERY_COUNT_COLUMN} from filtered_query) as filtered_query_count" if page || per_page - query_params = query_params.merge(filter_params).symbolize_keys - - sql = %( - with filtered_query as (#{filtered_query(config)}) - select * - from filtered_query - #{full_count_join} - #{limit} - ) - - model.find_by_sql([sql, query_params]) - end - - def self.sql_query_count(sql_query_results) - sql_query_results.empty? ? 0 : sql_query_results.first[QUERY_COUNT_COLUMN] - end - - def self.single_record_query(config) - results = sql_query(config) - results.first - end - - private - - def self.filtered_query(config) - query = config[:query] - filter_string = config[:filter_string] - sort_string = config[:sort_string] - sort_string = "order by #{sort_string}" if !sort_string.blank? - - sql = %( - with query as (#{query}) - select * - from query - #{filter_string} - #{sort_string} - ) - end - end -end diff --git a/lib/pattern_query_helper/url_helper.rb b/lib/pattern_query_helper/url_helper.rb new file mode 100644 index 0000000..0e81d54 --- /dev/null +++ b/lib/pattern_query_helper/url_helper.rb @@ -0,0 +1,21 @@ +require 'rails' +require 'active_support/dependencies' +require 'active_support/concern' + +module PatternQueryHelper + module AuthRequest + extend ActiveSupport::Concern + + included do + def query_helper_params + { + filters: params[:filter], + sorts: params[:sort], + page: params[:page], + per_page: params[:per_page], + associations: params[:include] + } + end + end + end +end diff --git a/spec/pattern_query_helper/associations_spec.rb b/spec/pattern_query_helper/associations_spec.rb index d6d1057..9d47265 100644 --- a/spec/pattern_query_helper/associations_spec.rb +++ b/spec/pattern_query_helper/associations_spec.rb @@ -13,7 +13,7 @@ it "loads associations" do associations = PatternQueryHelper::Associations.process_association_params("parent") payload = Child.all - results = PatternQueryHelper::Associations.load_associations(payload, associations, nil) + results = PatternQueryHelper::Associations.load_associations(payload: payload, associations: associations) results.each do |child| expect(child["parent_id"]).to eq(child["parent"]["id"]) end diff --git a/spec/pattern_query_helper/filter_spec.rb b/spec/pattern_query_helper/filter_spec.rb index aefe8a4..e100f7d 100644 --- a/spec/pattern_query_helper/filter_spec.rb +++ b/spec/pattern_query_helper/filter_spec.rb @@ -8,13 +8,13 @@ described_class.new( operator_code: "gte", criterion: Time.now, - column: "children.age" + comparate: "children.age" ) end it "creates sql string" do sql_string = filter.sql_string() - expect(sql_string).to eq("#{filter.column} #{filter.operator} #{filter.criterion}") + expect(sql_string).to eq("#{filter.comparate} #{filter.operator} :#{filter.bind_variable}") end it "creates array correctly for in/not in" @@ -31,7 +31,7 @@ # filter = described_class.new( # operator_code: code, # criterion: Faker::Number.between(0, 100), - # column: "children.age" + # comparate: "children.age" # ) # expect(filter.operator).to_not be_nil # end @@ -44,7 +44,7 @@ described_class.new( operator_code: "fake_code", criterion: Faker::Number.between(0, 100), - column: "children.age" + comparate: "children.age" ) }.to raise_error(ArgumentError) end @@ -71,7 +71,7 @@ described_class.new( operator_code: "gte", criterion: Faker::Number.between(0, 100), - column: "children.age" + comparate: "children.age" ) end end @@ -83,7 +83,7 @@ described_class.new( operator_code: "gte", criterion: Date.today, - column: "children.age" + comparate: "children.age" ) end end @@ -95,7 +95,7 @@ described_class.new( operator_code: "gte", criterion: Time.now, - column: "children.age" + comparate: "children.age" ) end end @@ -107,7 +107,7 @@ described_class.new( operator_code: "gte", criterion: "hello", - column: "children.age" + comparate: "children.age" ) end end @@ -119,7 +119,7 @@ described_class.new( operator_code: "in", criterion: [1,2,3,4], - column: "children.age" + comparate: "children.age" ) end end @@ -131,7 +131,7 @@ described_class.new( operator_code: "in", criterion: Date.today, - column: "children.age" + comparate: "children.age" ) end end @@ -143,7 +143,7 @@ described_class.new( operator_code: "null", criterion: true, - column: "children.age" + comparate: "children.age" ) end end @@ -155,7 +155,7 @@ described_class.new( operator_code: "null", criterion: false, - column: "children.age" + comparate: "children.age" ) end end @@ -167,7 +167,7 @@ described_class.new( operator_code: "null", criterion: "stringything", - column: "children.age" + comparate: "children.age" ) end end diff --git a/spec/pattern_query_helper/filtering_spec.rb b/spec/pattern_query_helper/filtering_spec.rb deleted file mode 100644 index af210c9..0000000 --- a/spec/pattern_query_helper/filtering_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require "spec_helper" - -RSpec.describe PatternQueryHelper::Filtering do - - describe "parse_pagination_params" do - it "create filters" do - filters = PatternQueryHelper::Filtering.create_filters(filters: { - "id" => { - "gte" => 20, - "lt" => 40, - "in" => "20,25,30", - "null" => false - }, - "name" => { - "like" => "my_name%" - } - }, valid_columns_map: {"id" => "id", "name" => "name"}) - expect(filters[:filter_string]).to eq("where id >= :id_gte\n and id < :id_lt\n and id in (:id_in)\n and id is not null\n and lower(name) like :name_like") - expect(filters[:filter_params]).to eq({"id_gte"=>20, "id_in"=>["20","25","30"], "id_lt"=>40, "name_like"=>"my_name%"}) - expect(filters[:filter_array]).to eq([ - {:column=>"id", :operator=>">=", :value=>20, :symbol=>"id_gte"}, - {:column=>"id", :operator=>"<", :value=>40, :symbol=>"id_lt"}, - {:column=>"id", :operator=>"in (:id_in)", :value=>["20","25","30"], :symbol=>"id_in"}, - {:column=>"id", :operator=>"is not null", :value=>false, :symbol=>""}, - {:column=>"name", :operator=>"like", :value=>"my_name%", :symbol=>"name_like"}, - ]) - end - it "handles a single filter" do - filters = PatternQueryHelper::Filtering.create_filters(filters: { - "id" => { - "gte" => 20 - } - }, valid_columns_map: {"id" => "id"}) - expect(filters[:filter_string]).to eq("where id >= :id_gte") - expect(filters[:filter_params]).to eq({"id_gte"=>20}) - expect(filters[:filter_array]).to eq([ - {:column=>"id", :operator=>">=", :value=>20, :symbol=>"id_gte"}, - ]) - end - it "handles no filter" do - filters = PatternQueryHelper::Filtering.create_filters(filters: { - }, include_where: false) - expect(filters[:filter_string]).to eq("1 = 1") - expect(filters[:filter_params]).to eq({}) - expect(filters[:filter_array]).to eq([]) - end - end -end diff --git a/spec/pattern_query_helper/pagination_spec.rb b/spec/pattern_query_helper/pagination_spec.rb deleted file mode 100644 index 20d3623..0000000 --- a/spec/pattern_query_helper/pagination_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -require "spec_helper" - -RSpec.describe PatternQueryHelper::Pagination do - before(:each) do - @per_page = Faker::Number.between(5,15) - @page = Faker::Number.between(2,5) - end - - describe "parse_pagination_params" do - - it "creates pagination params from url params" do - pagination_params = PatternQueryHelper::Pagination.parse_pagination_params(@page, @per_page) - expect(pagination_params[:per_page]).to eq(@per_page) - expect(pagination_params[:page]).to eq(@page) - end - - it "fails if page <= 0" do - expect { PatternQueryHelper::Pagination.parse_pagination_params(0, @per_page) }.to raise_error(RangeError) - end - - it "fails if per_page <= 0" do - expect { PatternQueryHelper::Pagination.parse_pagination_params(@page, 0) }.to raise_error(RangeError) - end - - it "default per page is 20" do - pagination_params = PatternQueryHelper::Pagination.parse_pagination_params(@page, nil) - expect(pagination_params[:per_page]).to eq(20) - end - - it "default page is 1" do - pagination_params = PatternQueryHelper::Pagination.parse_pagination_params(nil, @per_page) - expect(pagination_params[:page]).to eq(1) - end - - end - - describe "create_pagination_payload" do - - before(:each) do - @pagination_params = PatternQueryHelper::Pagination.parse_pagination_params(@page, @per_page) - @count = Faker::Number.between(100, 500) - @pagination_payload = PatternQueryHelper::Pagination.create_pagination_payload(@count, @pagination_params) - expect(@pagination_payload[:previous_page]).to eq(@page - 1) - expect(@pagination_payload[:next_page]).to eq(@page + 1) - end - - it "calculate total_pages correctly" do - expect(@pagination_payload[:total_pages]).to eq((@count/@per_page.to_f).ceil) - end - - it "next_page is null if on last page" do - @pagination_params[:page] = @pagination_payload[:total_pages] - @pagination_payload = PatternQueryHelper::Pagination.create_pagination_payload(@count, @pagination_params) - expect(@pagination_payload[:next_page]).to eq(nil) - expect(@pagination_payload[:last_page]).to eq(true) - end - - it "previous_page is null if on first page" do - @pagination_params[:page] = 1 - @pagination_payload = PatternQueryHelper::Pagination.create_pagination_payload(@count, @pagination_params) - expect(@pagination_payload[:previous_page]).to eq(nil) - expect(@pagination_payload[:first_page]).to eq(true) - end - - it "out_of_range determined correctly" do - @pagination_params[:page] = @pagination_payload[:total_pages] + 1 - @pagination_payload = PatternQueryHelper::Pagination.create_pagination_payload(@count, @pagination_params) - expect(@pagination_payload[:out_of_range]).to eq(true) - end - - end - -end diff --git a/spec/pattern_query_helper/query_string_spec.rb b/spec/pattern_query_helper/query_string_spec.rb index c65f361..1788a75 100644 --- a/spec/pattern_query_helper/query_string_spec.rb +++ b/spec/pattern_query_helper/query_string_spec.rb @@ -29,70 +29,5 @@ described_class.new(query) end - describe ".add_where_filters" do - let(:where_filters) {["a = a", "b = b", "c = c"]} - context "simple query" do - it "correctly sets indexes" do - simple_query.add_where_filters(where_filters) - expected_result = simple_query.modified_query_string.include?("where #{where_filters.join(" and ")}") - expect(expected_result).to be true - end - end - context "complex query" do - it "correctly sets indexes" do - complex_query.add_where_filters(where_filters) - expected_result = complex_query.modified_query_string.include?("where string = string and #{where_filters.join(" and ")}") - expect(expected_result).to be true - end - end - end - - describe ".add_sorting" do - let(:sorts) {["a desc", "b asc"]} - context "simple query" do - it "correctly adds custom sorting" do - simple_query.add_sorting(sorts) - expected_result = simple_query.modified_query_string.include?("order by #{sorts.join(", ")}") - expect(expected_result).to be true - end - end - context "complex query" do - it "correctly adds custom sorting" do - complex_query.add_sorting(sorts) - expected_result = complex_query.modified_query_string.include?("order by #{sorts.join(", ")}") - expect(expected_result).to be true - end - end - end - - describe ".add_having_filters" do - let(:having_filters) {["count(a) > 0", "sum(b) < 100"]} - context "simple query" do - it "raises error when no group by clause" do - expect{simple_query.add_having_filters(having_filters)}.to raise_error(ArgumentError) - end - end - context "simple group by query" do - it "correctly sets additional having filters" do - simple_group_by_query.add_having_filters(having_filters) - expected_result = simple_group_by_query.modified_query_string.include?("having #{having_filters.join(" and ")}") - expect(expected_result).to be true - end - end - context "complex query" do - it "correctly sets additional having filters" do - complex_query.add_having_filters(having_filters) - expected_result = complex_query.modified_query_string.include?("having sum(e) > 1 and #{having_filters.join(" and ")}") - expect(expected_result).to be true - end - end - end - - describe ".add_pagination" do - it "adds correct pagination to query" do - complex_query.add_pagination(2,3) - expected_result = complex_query.modified_query_string.include?("limit 3 offset 3") - expect(expected_result).to be true - end - end + it "pending tests" end diff --git a/spec/pattern_query_helper/sorting_spec.rb b/spec/pattern_query_helper/sorting_spec.rb deleted file mode 100644 index cee0772..0000000 --- a/spec/pattern_query_helper/sorting_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require "spec_helper" - -RSpec.describe PatternQueryHelper::Sorting do - - describe "parse_sorting_params" do - it "parses params into sorting sql string" do - sort_string = PatternQueryHelper::Sorting.parse_sorting_params("name:desc", ["name"]) - expect(sort_string).to eq("name desc") - end - - it "lowercase if asked for" do - sort_string = PatternQueryHelper::Sorting.parse_sorting_params("name:desc:lowercase", ["name"]) - expect(sort_string).to eq("lower(name) desc") - end - end -end diff --git a/spec/pattern_query_helper/sql_spec.rb b/spec/pattern_query_helper/sql_spec.rb deleted file mode 100644 index 89013de..0000000 --- a/spec/pattern_query_helper/sql_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -require "spec_helper" - -RSpec.describe PatternQueryHelper::Sql do - - describe "sql_query" do - it "query returns the same number of results as designated by per_page" do - per_page = Faker::Number.between(2,10) - results = PatternQueryHelper::Sql.sql_query( - model: Child, - query: "select * from children c join parents p on p.id = c.parent_id", - page: Faker::Number.between(1,10), - per_page: per_page - ) - - expect(results.length).to eq(per_page) - end - - it "query sorts correctly" do - sort_string = PatternQueryHelper::Sorting.parse_sorting_params("id:desc", ["id"]) - results = PatternQueryHelper::Sql.sql_query( - model: Child, - query: "select * from children c join parents p on p.id = c.parent_id", - page: 1, - per_page: 500, - sort_string: sort_string - ) - previous_id = 1000000000 - results.each do |result| - expect(result.id).to be < previous_id - previous_id = result.id - end - end - - it "query filters correctly" do - filters = PatternQueryHelper::Filtering.create_filters(filters: { - "id" => { - "gte" => 20, - "lt" => 40 - } - }) - results = PatternQueryHelper::Sql.sql_query( - model: Child, - query: "select * from children c join parents p on p.id = c.parent_id", - page: 1, - per_page: 500, - filter_string: filters[:filter_string], - filter_params: filters[:filter_params] - ) - results.each do |result| - expect(result.id).to be >= 20 - expect(result.id).to be < 40 - end - end - - it "query returns all results if no pagination info" do - results = PatternQueryHelper::Sql.sql_query( - model: Child, - query: "select * from children c join parents p on p.id = c.parent_id", - ) - expect(results.length).to eq(Child.all.length) - end - end - - describe "sql_query_count" do - it "should count the number of rows correctly" do - results = PatternQueryHelper::Sql.sql_query( - model: Child, - query: "select * from children c join parents p on p.id = c.parent_id", - page: 1 - ) - count = PatternQueryHelper::Sql.sql_query_count(results) - expect(count).to eq(Child.all.length) - end - - it "should count the number of rows correctly with filters in place" do - filters = PatternQueryHelper::Filtering.create_filters(filters: { - "id" => { - "gte" => 20, - "lt" => 40 - } - }) - results = PatternQueryHelper::Sql.sql_query( - model: Child, - query: "select * from children c join parents p on p.id = c.parent_id", - filter_string: filters[:filter_string], - filter_params: filters[:filter_params], - page: 1, - ) - count = PatternQueryHelper::Sql.sql_query_count(results) - expect(count).to eq(Child.all.where("id >= 20 and id < 40").length) - end - end - - describe "single_record_query" do - it "returns one result" do - result = PatternQueryHelper::Sql.single_record_query( - model: Child, - query: "select * from children c join parents p on p.id = c.parent_id where c.id = 1", - ) - expect(result.class).to eq(Child) - end - end -end diff --git a/spec/pattern_query_helper_spec.rb b/spec/pattern_query_helper_spec.rb index a102c44..484155a 100644 --- a/spec/pattern_query_helper_spec.rb +++ b/spec/pattern_query_helper_spec.rb @@ -1,39 +1,3 @@ RSpec.describe PatternQueryHelper do - it "returns a paginated sql query" do - results = PatternQueryHelper.run_sql_query(Child, "select * from children", {}, @url_params, ["name", "id"]) - expect(results[:pagination]).to_not be nil - expect(results[:data]).to_not be nil - end - - it "returns a sql query" do - results = PatternQueryHelper.run_sql_query(Child, "select * from children", {}, @url_params, ["name", "id"]) - expect(results[:data]).to_not be nil - end - - it "returns a single record sql query" do - @url_params[:filter] = nil - results = PatternQueryHelper.run_sql_query(Child, "select * from children where id=#{Child.first.id}", {}, @url_params, ["name", "id"], true) - expect(results[:data]).to_not be nil - end - - it "returns an active record query" do - results = PatternQueryHelper.run_active_record_query(Child.all, @url_params, ["name", "id"]) - expect(results[:data]).to_not be nil - end - - it "returns a paginated active record query" do - results = PatternQueryHelper.run_active_record_query(Child.all, @url_params, ["name", "id"]) - expect(results[:pagination]).to_not be nil - expect(results[:data]).to_not be nil - end - - it "has a version number" do - expect(PatternQueryHelper::VERSION).not_to be nil - end - - it "sets up the test database correctly" do - expect(Parent.all.count).to eq(100) - # Every parent has between 2 and 6 children - expect(Child.all.count).to be_between(200, 600).inclusive - end + it "pending tests" end From 2bf458fc64aeca30d069dcc7dfd5a2ba6c814139 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 18 Jun 2019 14:23:19 -0600 Subject: [PATCH 10/48] url params helper --- lib/pattern_query_helper/{url_helper.rb => url_params.rb} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename lib/pattern_query_helper/{url_helper.rb => url_params.rb} (95%) diff --git a/lib/pattern_query_helper/url_helper.rb b/lib/pattern_query_helper/url_params.rb similarity index 95% rename from lib/pattern_query_helper/url_helper.rb rename to lib/pattern_query_helper/url_params.rb index 0e81d54..4df0422 100644 --- a/lib/pattern_query_helper/url_helper.rb +++ b/lib/pattern_query_helper/url_params.rb @@ -3,7 +3,7 @@ require 'active_support/concern' module PatternQueryHelper - module AuthRequest + module UrlParams extend ActiveSupport::Concern included do From 24c4411f8eb0a3b515ab7646793fc33ec2386474 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 18 Jun 2019 14:28:42 -0600 Subject: [PATCH 11/48] query helper concern --- lib/pattern_query_helper/url_params.rb | 21 --------------------- lib/query_helper_concern.rb | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 21 deletions(-) delete mode 100644 lib/pattern_query_helper/url_params.rb create mode 100644 lib/query_helper_concern.rb diff --git a/lib/pattern_query_helper/url_params.rb b/lib/pattern_query_helper/url_params.rb deleted file mode 100644 index 4df0422..0000000 --- a/lib/pattern_query_helper/url_params.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'rails' -require 'active_support/dependencies' -require 'active_support/concern' - -module PatternQueryHelper - module UrlParams - extend ActiveSupport::Concern - - included do - def query_helper_params - { - filters: params[:filter], - sorts: params[:sort], - page: params[:page], - per_page: params[:per_page], - associations: params[:include] - } - end - end - end -end diff --git a/lib/query_helper_concern.rb b/lib/query_helper_concern.rb new file mode 100644 index 0000000..bd6b8b3 --- /dev/null +++ b/lib/query_helper_concern.rb @@ -0,0 +1,19 @@ +require 'rails' +require 'active_support/dependencies' +require 'active_support/concern' + +module QueryHelperConcern + extend ActiveSupport::Concern + + included do + def query_helper_params + { + filters: params[:filter], + sorts: params[:sort], + page: params[:page], + per_page: params[:per_page], + associations: params[:include] + } + end + end +end From 3d614b78db11763ddd21b16459931ec75631709e Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 18 Jun 2019 14:41:37 -0600 Subject: [PATCH 12/48] query helper concern --- lib/pattern_query_helper.rb | 1 + .../query_helper_concern.rb | 21 +++++++++++++++++++ lib/query_helper_concern.rb | 19 ----------------- 3 files changed, 22 insertions(+), 19 deletions(-) create mode 100644 lib/pattern_query_helper/query_helper_concern.rb delete mode 100644 lib/query_helper_concern.rb diff --git a/lib/pattern_query_helper.rb b/lib/pattern_query_helper.rb index ce0bd1c..b1f4786 100644 --- a/lib/pattern_query_helper.rb +++ b/lib/pattern_query_helper.rb @@ -6,6 +6,7 @@ require "pattern_query_helper/query_filter" require "pattern_query_helper/sort" require "pattern_query_helper/associations" +require "pattern_query_helper/query_helper_concern" module PatternQueryHelper diff --git a/lib/pattern_query_helper/query_helper_concern.rb b/lib/pattern_query_helper/query_helper_concern.rb new file mode 100644 index 0000000..f8d6098 --- /dev/null +++ b/lib/pattern_query_helper/query_helper_concern.rb @@ -0,0 +1,21 @@ +require 'rails' +require 'active_support/dependencies' +require 'active_support/concern' + +module PatternQueryHelper + module QueryHelperConcern + extend ActiveSupport::Concern + + included do + def query_helper_params + { + filters: params[:filter], + sorts: params[:sort], + page: params[:page], + per_page: params[:per_page], + associations: params[:include] + } + end + end + end +end diff --git a/lib/query_helper_concern.rb b/lib/query_helper_concern.rb deleted file mode 100644 index bd6b8b3..0000000 --- a/lib/query_helper_concern.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'rails' -require 'active_support/dependencies' -require 'active_support/concern' - -module QueryHelperConcern - extend ActiveSupport::Concern - - included do - def query_helper_params - { - filters: params[:filter], - sorts: params[:sort], - page: params[:page], - per_page: params[:per_page], - associations: params[:include] - } - end - end -end From ee68edc0996a5061dc85a0fe1ba2b5427fcf54a2 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 18 Jun 2019 15:18:10 -0600 Subject: [PATCH 13/48] change to query helper concern --- lib/pattern_query_helper/query_helper_concern.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/pattern_query_helper/query_helper_concern.rb b/lib/pattern_query_helper/query_helper_concern.rb index f8d6098..c85b0fd 100644 --- a/lib/pattern_query_helper/query_helper_concern.rb +++ b/lib/pattern_query_helper/query_helper_concern.rb @@ -8,13 +8,13 @@ module QueryHelperConcern included do def query_helper_params - { - filters: params[:filter], - sorts: params[:sort], - page: params[:page], - per_page: params[:per_page], - associations: params[:include] - } + helpers = {} + helpers[:filters] = params[:filter] if params[:filter] + helpers[:sorts] = params[:sort] if params[:sort] + helpers[:page] = params[:page] if params[:page] + helpers[:per_page] = params[:per_page] if params[:per_page] + helpers[:associations] = params[:include] if params[:include] + helpers end end end From 89f2269ae7b79bfa992b97737c9c3c2ebd8b1337 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 18 Jun 2019 19:32:58 -0600 Subject: [PATCH 14/48] don't lowercase "true" and "false" --- lib/pattern_query_helper/filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pattern_query_helper/filter.rb b/lib/pattern_query_helper/filter.rb index 8365d71..4f58bc5 100644 --- a/lib/pattern_query_helper/filter.rb +++ b/lib/pattern_query_helper/filter.rb @@ -76,7 +76,7 @@ def mofify_criterion def modify_comparate # lowercase strings for comparison - @comparate = "lower(#{@comparate})" if criterion.class == String && criterion.scan(/[a-zA-Z]/).any? + @comparate = "lower(#{@comparate})" if criterion.class == String && criterion.scan(/[a-zA-Z]/).any? && !["true", "false"].include?(criterion) end def validate_criterion From 1fcd6fc167145531d361f242dffb2f93ccf47b80 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Wed, 19 Jun 2019 09:56:39 -0600 Subject: [PATCH 15/48] account for subqueries when determining insert indexes --- lib/pattern_query_helper/query_string.rb | 27 +++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/pattern_query_helper/query_string.rb b/lib/pattern_query_helper/query_string.rb index d98cbff..20e9199 100644 --- a/lib/pattern_query_helper/query_string.rb +++ b/lib/pattern_query_helper/query_string.rb @@ -22,22 +22,29 @@ def initialize( end def calculate_indexes - @last_select_index = @sql.rindex(/[Ss][Ee][Ll][Ee][Cc][Tt]/) - @last_from_index = @sql.rindex(/[Ff][Rr][Oo][Mm]/) - @last_where_index = @sql.index(/[Ww][Hh][Ee][Rr][Ee]/, @last_select_index) - @last_group_by_index = @sql.index(/[Gg][Rr][Oo][Uu][Pp] [Bb][Yy]/, @last_select_index) - @last_having_index = @sql.index(/[Hh][Aa][Vv][Ii][Nn][Gg]/, @last_select_index) - @last_order_by_index = @sql.index(/[Oo][Rr][Dd][Ee][Rr] [Bb][Yy]/, @last_select_index) + # Replace everything between () to find indexes. + # This will allow us to ignore subueries when determing indexes + white_out_sql = @sql.dup + while white_out_sql.scan(/\([^()]*\)/).length > 0 do + white_out_sql.scan(/\([^()]*\)/).each { |s| white_out_sql.gsub!(s,s.gsub(/./, '*')) } + end + + @last_select_index = white_out_sql.rindex(/(? Date: Fri, 21 Jun 2019 10:47:13 -0600 Subject: [PATCH 16/48] add ActiveRecordQuery class --- lib/pattern_query_helper.rb | 1 + .../active_record_query.rb | 34 +++++++++++++++++++ lib/pattern_query_helper/sql_query.rb | 19 ++++++----- 3 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 lib/pattern_query_helper/active_record_query.rb diff --git a/lib/pattern_query_helper.rb b/lib/pattern_query_helper.rb index b1f4786..38aee4d 100644 --- a/lib/pattern_query_helper.rb +++ b/lib/pattern_query_helper.rb @@ -3,6 +3,7 @@ require "pattern_query_helper/column_map" require "pattern_query_helper/query_string" require "pattern_query_helper/sql_query" +require "pattern_query_helper/active_record_query" require "pattern_query_helper/query_filter" require "pattern_query_helper/sort" require "pattern_query_helper/associations" diff --git a/lib/pattern_query_helper/active_record_query.rb b/lib/pattern_query_helper/active_record_query.rb new file mode 100644 index 0000000..c31714c --- /dev/null +++ b/lib/pattern_query_helper/active_record_query.rb @@ -0,0 +1,34 @@ +module PatternQueryHelper + class ActiveRecordQuery < SqlQuery + + def initialize( + active_record_call:, # the active_record_query to be executed + query_params: {}, # a list of bind variables to be embedded into the query + column_mappings: {}, # A hash that translates aliases to sql expressions + filters: {}, # a list of filters in the form of {"comparate_alias"=>{"operator_code"=>"value"}}. i.e. {"age"=>{"lt"=>100}, "children_count"=>{"gt"=>0}} + sorts: "", # a comma separated string with a list of sort values i.e "age:desc,name:asc:lowercase" + page: nil, # define the page you want returned + per_page: nil, # define how many results you want per page + single_record: false, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned + associations: [], # a list of activerecord associations you'd like included in the payload + as_json_options: {}, # a list of as_json options you'd like run before returning the payload + run: true # whether or not you'd like to run the query on initilization + ) + + super( + model: active_record_call.model, + query: active_record_call.to_sql, + query_params: query_params, + column_mappings: column_mappings, + filters: filters, + sorts: sorts, + page: page, + per_page: per_page, + single_record: single_record, + associations: associations, + as_json_options: as_json_options, + run: run + ) + end + end +end diff --git a/lib/pattern_query_helper/sql_query.rb b/lib/pattern_query_helper/sql_query.rb index a1b3172..4dee8ce 100644 --- a/lib/pattern_query_helper/sql_query.rb +++ b/lib/pattern_query_helper/sql_query.rb @@ -6,15 +6,16 @@ class SqlQuery def initialize( model:, # the model to run the query against query:, # the custom sql to be executed - query_params: {}, + query_params: {}, # a list of bind variables to be embedded into the query column_mappings: {}, # A hash that translates aliases to sql expressions - filters: {}, - sorts: "", - page: nil, - per_page: nil, - single_record: false, - associations: [], - as_json_options: {} + filters: {}, # a list of filters in the form of {"comparate_alias"=>{"operator_code"=>"value"}}. i.e. {"age"=>{"lt"=>100}, "children_count"=>{"gt"=>0}} + sorts: "", # a comma separated string with a list of sort values i.e "age:desc,name:asc:lowercase" + page: nil, # define the page you want returned + per_page: nil, # define how many results you want per page + single_record: false, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned + associations: [], # a list of activerecord associations you'd like included in the payload + as_json_options: {}, # a list of as_json options you'd like run before returning the payload + run: true # whether or not you'd like to run the query on initilization ) @model = model @query_params = query_params @@ -35,7 +36,7 @@ def initialize( per_page: @per_page ) @query_params.merge!(@query_filter.bind_variables) - execute_query() + execute_query() if run end def execute_query From d67b18ff03b5e33920e7f5cd63067d177d78e398 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Fri, 21 Jun 2019 10:49:26 -0600 Subject: [PATCH 17/48] rename classes --- .../{active_record_query.rb => active_record.rb} | 4 ++-- lib/pattern_query_helper/{sql_query.rb => sql.rb} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename lib/pattern_query_helper/{active_record_query.rb => active_record.rb} (97%) rename lib/pattern_query_helper/{sql_query.rb => sql.rb} (99%) diff --git a/lib/pattern_query_helper/active_record_query.rb b/lib/pattern_query_helper/active_record.rb similarity index 97% rename from lib/pattern_query_helper/active_record_query.rb rename to lib/pattern_query_helper/active_record.rb index c31714c..ece4cd2 100644 --- a/lib/pattern_query_helper/active_record_query.rb +++ b/lib/pattern_query_helper/active_record.rb @@ -1,5 +1,5 @@ module PatternQueryHelper - class ActiveRecordQuery < SqlQuery + class ActiveRecord < Sql def initialize( active_record_call:, # the active_record_query to be executed @@ -29,6 +29,6 @@ def initialize( as_json_options: as_json_options, run: run ) - end + end end end diff --git a/lib/pattern_query_helper/sql_query.rb b/lib/pattern_query_helper/sql.rb similarity index 99% rename from lib/pattern_query_helper/sql_query.rb rename to lib/pattern_query_helper/sql.rb index 4dee8ce..ef11939 100644 --- a/lib/pattern_query_helper/sql_query.rb +++ b/lib/pattern_query_helper/sql.rb @@ -1,5 +1,5 @@ module PatternQueryHelper - class SqlQuery + class Sql attr_accessor :model, :query_string, :query_params, :query_filter, :results From 99ff56103b0cf352b3a20e68ee2df3a6e72a9687 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Fri, 21 Jun 2019 11:38:57 -0600 Subject: [PATCH 18/48] Updates --- Gemfile.lock | 34 +------------------ lib/pattern_query_helper.rb | 4 +-- .../query_helper_concern.rb | 2 -- pattern_query_helper.gemspec | 2 +- .../{sql_query_spec.rb => sql_spec.rb} | 2 +- 5 files changed, 5 insertions(+), 39 deletions(-) rename spec/pattern_query_helper/{sql_query_spec.rb => sql_spec.rb} (96%) diff --git a/Gemfile.lock b/Gemfile.lock index 84bcaee..32a1ab0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,17 +3,11 @@ PATH specs: pattern_query_helper (0.2.10) activerecord (~> 5.0) - kaminari (~> 1.1.1) + activesupport (~> 5.0) GEM remote: https://rubygems.org/ specs: - actionview (5.2.3) - activesupport (= 5.2.3) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) activemodel (5.2.3) activesupport (= 5.2.3) activerecord (5.2.3) @@ -26,40 +20,14 @@ GEM minitest (~> 5.1) tzinfo (~> 1.1) arel (9.0.0) - builder (3.2.3) byebug (11.0.1) concurrent-ruby (1.1.4) - crass (1.0.4) diff-lcs (1.3) - erubi (1.8.0) faker (1.9.3) i18n (>= 0.7) i18n (1.6.0) concurrent-ruby (~> 1.0) - kaminari (1.1.1) - activesupport (>= 4.1.0) - kaminari-actionview (= 1.1.1) - kaminari-activerecord (= 1.1.1) - kaminari-core (= 1.1.1) - kaminari-actionview (1.1.1) - actionview - kaminari-core (= 1.1.1) - kaminari-activerecord (1.1.1) - activerecord - kaminari-core (= 1.1.1) - kaminari-core (1.1.1) - loofah (2.2.3) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mini_portile2 (2.4.0) minitest (5.11.3) - nokogiri (1.10.3) - mini_portile2 (~> 2.4.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.0.4) - loofah (~> 2.2, >= 2.2.2) rake (10.5.0) rspec (3.8.0) rspec-core (~> 3.8.0) diff --git a/lib/pattern_query_helper.rb b/lib/pattern_query_helper.rb index 38aee4d..27ea39f 100644 --- a/lib/pattern_query_helper.rb +++ b/lib/pattern_query_helper.rb @@ -2,8 +2,8 @@ require "pattern_query_helper/filter" require "pattern_query_helper/column_map" require "pattern_query_helper/query_string" -require "pattern_query_helper/sql_query" -require "pattern_query_helper/active_record_query" +require "pattern_query_helper/sql" +require "pattern_query_helper/active_record" require "pattern_query_helper/query_filter" require "pattern_query_helper/sort" require "pattern_query_helper/associations" diff --git a/lib/pattern_query_helper/query_helper_concern.rb b/lib/pattern_query_helper/query_helper_concern.rb index c85b0fd..cb4d021 100644 --- a/lib/pattern_query_helper/query_helper_concern.rb +++ b/lib/pattern_query_helper/query_helper_concern.rb @@ -1,5 +1,3 @@ -require 'rails' -require 'active_support/dependencies' require 'active_support/concern' module PatternQueryHelper diff --git a/pattern_query_helper.gemspec b/pattern_query_helper.gemspec index f5d318c..b706034 100644 --- a/pattern_query_helper.gemspec +++ b/pattern_query_helper.gemspec @@ -44,5 +44,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency "byebug" spec.add_dependency "activerecord", "~> 5.0" - spec.add_dependency "kaminari", "~> 1.1.1" + spec.add_dependency "activesupport", "~> 5.0" end diff --git a/spec/pattern_query_helper/sql_query_spec.rb b/spec/pattern_query_helper/sql_spec.rb similarity index 96% rename from spec/pattern_query_helper/sql_query_spec.rb rename to spec/pattern_query_helper/sql_spec.rb index c5d8753..ea7b960 100644 --- a/spec/pattern_query_helper/sql_query_spec.rb +++ b/spec/pattern_query_helper/sql_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -RSpec.describe PatternQueryHelper::SqlQuery do +RSpec.describe PatternQueryHelper::Sql do let(:query) do %{ select parents.id, parents.name, count(children.id) as children_count From a777276701daf89c6ccafeb6fd0e8fd23270927a Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Wed, 26 Jun 2019 15:53:17 -0600 Subject: [PATCH 19/48] updates to remove comments, improve regex to find keywords, autofind aliases --- lib/pattern_query_helper/column_map.rb | 1 - lib/pattern_query_helper/query_string.rb | 69 +++++++++++++++++++----- lib/pattern_query_helper/sort.rb | 2 +- lib/pattern_query_helper/sql.rb | 25 +++++++-- spec/pattern_query_helper/sql_spec.rb | 8 +-- 5 files changed, 83 insertions(+), 22 deletions(-) diff --git a/lib/pattern_query_helper/column_map.rb b/lib/pattern_query_helper/column_map.rb index b4d7023..95c5b9d 100644 --- a/lib/pattern_query_helper/column_map.rb +++ b/lib/pattern_query_helper/column_map.rb @@ -32,6 +32,5 @@ def self.create_from_hash(hash) end map end - end end diff --git a/lib/pattern_query_helper/query_string.rb b/lib/pattern_query_helper/query_string.rb index 20e9199..bbea38b 100644 --- a/lib/pattern_query_helper/query_string.rb +++ b/lib/pattern_query_helper/query_string.rb @@ -1,7 +1,7 @@ module PatternQueryHelper class QueryString - attr_accessor :query_string, :where_filters, :having_filters, :sorts, :page, :per_page + attr_accessor :query_string, :where_filters, :having_filters, :sorts, :page, :per_page, :alias_map def initialize( sql:, @@ -15,26 +15,30 @@ def initialize( @where_filters = where_filters @having_filters = having_filters @sorts = sorts - @page = page - @per_page = per_page + @page = page.to_i if page # Turn into an integer to avoid any potential sql injection + @per_page = per_page.to_i if per_page # Turn into an integer to avoid any potential sql injection calculate_indexes() true end def calculate_indexes - # Replace everything between () to find indexes. - # This will allow us to ignore subueries when determing indexes + # Remove sql comments + @sql.gsub!(/\/\*(.*?)\*\//, '') + @sql.gsub!(/--(.*)$/, '') + + # Replace everything between () and '' and "" to find indexes. + # This will allow us to ignore subqueries and common table expressions when determining injection points white_out_sql = @sql.dup - while white_out_sql.scan(/\([^()]*\)/).length > 0 do - white_out_sql.scan(/\([^()]*\)/).each { |s| white_out_sql.gsub!(s,s.gsub(/./, '*')) } + while white_out_sql.scan(/\"[^""]*\"|\'[^'']*\'|\([^()]*\)/).length > 0 do + white_out_sql.scan(/\"[^""]*\"|\'[^'']*\'|\([^()]*\)/).each { |s| white_out_sql.gsub!(s,s.gsub(/./, '*')) } end - @last_select_index = white_out_sql.rindex(/(? {sql_expression: "count(children.id)", aggregate: true}, - "name" => "parents.name", - "age" => "parents.age" + # "name" => "parents.name", + # "age" => "parents.age" } end @@ -36,8 +36,10 @@ associations: includes, as_json_options: as_json_options, page: 1, - per_page: 5 + per_page: 5, + run: true ) + byebug results = sql_query.payload() expected_results = Parent.all.to_a.select{ |p| p.children.length > 1 && p.age < 100 } expect(results[:pagination][:count]).to eq(expected_results.length) From 437a7236e5c16f50fae2c93661d4b87a351378d5 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Thu, 27 Jun 2019 10:36:27 -0600 Subject: [PATCH 20/48] fixed some tests and fixed some bugs --- lib/pattern_query_helper.rb | 4 +- ...ctive_record.rb => active_record_query.rb} | 2 +- lib/pattern_query_helper/query_string.rb | 16 +++--- lib/pattern_query_helper/sql.rb | 53 ++++++++++--------- spec/pattern_query_helper/sql_spec.rb | 3 +- 5 files changed, 41 insertions(+), 37 deletions(-) rename lib/pattern_query_helper/{active_record.rb => active_record_query.rb} (97%) diff --git a/lib/pattern_query_helper.rb b/lib/pattern_query_helper.rb index 27ea39f..b6609f7 100644 --- a/lib/pattern_query_helper.rb +++ b/lib/pattern_query_helper.rb @@ -1,9 +1,11 @@ +require "active_record" + require "pattern_query_helper/version" require "pattern_query_helper/filter" require "pattern_query_helper/column_map" require "pattern_query_helper/query_string" require "pattern_query_helper/sql" -require "pattern_query_helper/active_record" +require "pattern_query_helper/active_record_query" require "pattern_query_helper/query_filter" require "pattern_query_helper/sort" require "pattern_query_helper/associations" diff --git a/lib/pattern_query_helper/active_record.rb b/lib/pattern_query_helper/active_record_query.rb similarity index 97% rename from lib/pattern_query_helper/active_record.rb rename to lib/pattern_query_helper/active_record_query.rb index ece4cd2..b3f0f3e 100644 --- a/lib/pattern_query_helper/active_record.rb +++ b/lib/pattern_query_helper/active_record_query.rb @@ -1,5 +1,5 @@ module PatternQueryHelper - class ActiveRecord < Sql + class ActiveRecordQuery < Sql def initialize( active_record_call:, # the active_record_query to be executed diff --git a/lib/pattern_query_helper/query_string.rb b/lib/pattern_query_helper/query_string.rb index bbea38b..827d7fb 100644 --- a/lib/pattern_query_helper/query_string.rb +++ b/lib/pattern_query_helper/query_string.rb @@ -117,17 +117,17 @@ def build end def update( - where_filters: [], - having_filters: [], - sorts: [], + where_filters: nil, + having_filters: nil, + sorts: nil, page: nil, per_page: nil ) - @where_filters = where_filters - @having_filters = having_filters - @sorts = sorts - @page = page - @per_page = per_page + @where_filters = where_filters if where_filters + @having_filters = having_filters if having_filters + @sorts = sorts if sorts + @page = page if page + @per_page = per_page if per_page end end end diff --git a/lib/pattern_query_helper/sql.rb b/lib/pattern_query_helper/sql.rb index f9b1248..e2d99fa 100644 --- a/lib/pattern_query_helper/sql.rb +++ b/lib/pattern_query_helper/sql.rb @@ -58,29 +58,26 @@ def initialize( def execute_query # Execute Sql Query - @results = @model.find_by_sql([@query_string.build(), @query_params]) - - # Determine total result count - @count = @page && @per_page && results.length > 0? results.first["_query_full_count"] : results.length - - # Return a single result if requested - @results = @results.first if @single_record + @results = @model.find_by_sql([@query_string.build(), @query_params]) # Execute Sql Query + @results = @results.first if @single_record # Return a single result if requested + determine_count() load_associations() clean_results() end - def load_associations - @results = PatternQueryHelper::Associations.load_associations( - payload: @results, - associations: @associations, - as_json_options: @as_json_options - ) + def payload + if @page && @per_page + { + pagination: pagination_results(), + data: @results + } + else + { data: @results } + end end - def clean_results - @results.map!{ |r| r.except("_query_full_count") } if @page && @per_page - end + private def pagination_results total_pages = (@count/(@per_page.nonzero? || 1).to_f).ceil @@ -103,15 +100,21 @@ def pagination_results } end - def payload - if @page && @per_page - { - pagination: pagination_results(), - data: @results - } - else - { data: @results } - end + def clean_results + @results.map!{ |r| r.except("_query_full_count") } if @page && @per_page + end + + def load_associations + @results = PatternQueryHelper::Associations.load_associations( + payload: @results, + associations: @associations, + as_json_options: @as_json_options + ) + end + + def determine_count + # Determine total result count (unpaginated) + @count = @page && @per_page && results.length > 0 ? results.first["_query_full_count"] : results.length end end end diff --git a/spec/pattern_query_helper/sql_spec.rb b/spec/pattern_query_helper/sql_spec.rb index 0e4b634..f299b01 100644 --- a/spec/pattern_query_helper/sql_spec.rb +++ b/spec/pattern_query_helper/sql_spec.rb @@ -3,7 +3,7 @@ RSpec.describe PatternQueryHelper::Sql do let(:query) do %{ - select parents.id, parents.name, count(children.id) as children_count + select parents.id, parents.name, parents.age, count(children.id) as children_count from parents join children on parents.id = children.parent_id group by parents.id @@ -39,7 +39,6 @@ per_page: 5, run: true ) - byebug results = sql_query.payload() expected_results = Parent.all.to_a.select{ |p| p.children.length > 1 && p.age < 100 } expect(results[:pagination][:count]).to eq(expected_results.length) From ebbcd7605af7d78ee1af2269ba7630fe12438a11 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Thu, 27 Jun 2019 11:27:13 -0600 Subject: [PATCH 21/48] Moved the logic to find default aliases to the column mappings class --- lib/pattern_query_helper/column_map.rb | 60 +++++++-- lib/pattern_query_helper/query_string.rb | 153 ++++++++++++----------- lib/pattern_query_helper/sql.rb | 22 ++-- 3 files changed, 138 insertions(+), 97 deletions(-) diff --git a/lib/pattern_query_helper/column_map.rb b/lib/pattern_query_helper/column_map.rb index 95c5b9d..419240a 100644 --- a/lib/pattern_query_helper/column_map.rb +++ b/lib/pattern_query_helper/column_map.rb @@ -1,16 +1,15 @@ module PatternQueryHelper class ColumnMap - attr_accessor :alias_name, :sql_expression, :aggregate + def self.create_column_mappings(custom_mappings:, query:) + default = find_aliases_in_query(query) + maps = create_from_hash(custom_mappings) - def initialize( - alias_name:, - sql_expression:, - aggregate: false - ) - @alias_name = alias_name - @sql_expression = sql_expression - @aggregate = aggregate + default.each do |m| + maps << m if maps.select{|x| x.alias_name == m.alias_name}.empty? + end + + maps end def self.create_from_hash(hash) @@ -32,5 +31,48 @@ def self.create_from_hash(hash) end map end + + def self.find_aliases_in_query(query) + # Determine alias expression combos. White out sql used in case there + # are any custom strings or subqueries in the select clause + select_index = PatternQueryHelper::QueryString.last_select_index(query) + from_index = PatternQueryHelper::QueryString.last_from_index(query) + white_out_select = PatternQueryHelper::QueryString.white_out_query(query)[select_index..from_index] + select_clause = query[select_index..from_index] + comma_split_points = white_out_select.each_char.with_index.map{|char, i| i if char == ','}.compact + comma_split_points.unshift(-1) # We need the first select clause to start out with a 'split' + column_maps = white_out_select.split(",").each_with_index.map do |x,i| + sql_alias = x.squish.split(" as ")[1] || x.squish.split(" AS ")[1] || x.squish.split(".")[1] # look for custom defined aliases or table.column notation + sql_alias = nil unless /^[a-zA-Z_]+$/.match?(sql_alias) # only allow aliases with letters and underscores + sql_expression = if x.split(" as ")[1] + expression_length = x.split(" as ")[0].length + select_clause[comma_split_points[i] + 1, expression_length] + elsif x.squish.split(" AS ")[1] + expression_length = x.split(" AS ")[0].length + select_clause[comma_split_points[i] + 1, expression_length] + elsif x.squish.split(".")[1] + select_clause[comma_split_points[i] + 1, x.length] + end + PatternQueryHelper::ColumnMap.new( + alias_name: sql_alias, + sql_expression: sql_expression.squish, + aggregate: /(array_agg|avg|bit_and|bit_or|bool_and|bool_or|count|every|json_agg|jsonb_agg|json_object_agg|jsonb_object_agg|max|min|string_agg|sum|xmlagg)\((.*)\)/.match?(sql_expression) + ) if sql_alias + end + column_maps.compact + end + + attr_accessor :alias_name, :sql_expression, :aggregate + + def initialize( + alias_name:, + sql_expression:, + aggregate: false + ) + @alias_name = alias_name + @sql_expression = sql_expression + @aggregate = aggregate + end + end end diff --git a/lib/pattern_query_helper/query_string.rb b/lib/pattern_query_helper/query_string.rb index 827d7fb..172b953 100644 --- a/lib/pattern_query_helper/query_string.rb +++ b/lib/pattern_query_helper/query_string.rb @@ -3,6 +3,50 @@ class QueryString attr_accessor :query_string, :where_filters, :having_filters, :sorts, :page, :per_page, :alias_map + # I've opted to make several methods class methods + # in order to utilize them in other parts of the gem + + def self.remove_comments(query) + # Remove sql comments + query.gsub(/\/\*(.*?)\*\//, '').gsub(/--(.*)$/, '') + end + + def self.white_out_query(query) + # Replace everything between () and '' and "" to find indexes. + # This will allow us to ignore subqueries and common table expressions when determining injection points + white_out = query.dup + while white_out.scan(/\"[^""]*\"|\'[^'']*\'|\([^()]*\)/).length > 0 do + white_out.scan(/\"[^""]*\"|\'[^'']*\'|\([^()]*\)/).each { |s| white_out.gsub!(s,s.gsub(/./, '*')) } + end + white_out + end + + def self.last_select_index(query) + # space or new line at beginning of select + # return index at the end of the word + query.rindex(/( |^)[Ss][Ee][Ll][Ee][Cc][Tt] /) + query[/( |^)[Ss][Ee][Ll][Ee][Cc][Tt] /].size + end + + def self.last_from_index(query) + query.index(/ [Ff][Rr][Oo][Mm] /, last_select_index(query)) + end + + def self.last_where_index(query) + query.index(/ [Ww][Hh][Ee][Rr][Ee] /, last_select_index(query)) + end + + def self.last_group_by_index(query) + query.index(/ [Gg][Rr][Oo][Uu][Pp] [Bb][Yy] /, last_select_index(query)) + end + + def self.last_having_index(query) + query.index(/ [Hh][Aa][Vv][Ii][Nn][Gg] /, last_select_index(query)) + end + + def self.last_order_by_index(query) + query.index(/ [Oo][Rr][Dd][Ee][Rr] [Bb][Yy ]/, last_select_index(query)) + end + def initialize( sql:, where_filters: [], @@ -11,7 +55,7 @@ def initialize( page: nil, per_page: nil ) - @sql = sql.squish + @sql = self.class.remove_comments(sql).squish @where_filters = where_filters @having_filters = having_filters @sorts = sorts @@ -21,60 +65,46 @@ def initialize( true end - def calculate_indexes - # Remove sql comments - @sql.gsub!(/\/\*(.*?)\*\//, '') - @sql.gsub!(/--(.*)$/, '') + def build + modified_sql = @sql.dup + modified_sql = modified_sql.slice(0, @last_order_by_index) if @order_by_included # Remove previous sorting if it exists + modified_sql.insert(modified_sql.length, pagination_insert) if @page && @per_page + modified_sql.insert(@insert_order_by_index, sort_insert) if @sorts && @sorts.length > 0 + modified_sql.insert(@insert_having_index, having_insert) if @having_filters && @having_filters.length > 0 + modified_sql.insert(@insert_where_index, where_insert) if @where_filters && @where_filters.length > 0 + modified_sql.insert(@insert_select_index, total_count_select_insert) if @page && @per_page + modified_sql.squish + end - # Replace everything between () and '' and "" to find indexes. - # This will allow us to ignore subqueries and common table expressions when determining injection points - white_out_sql = @sql.dup - while white_out_sql.scan(/\"[^""]*\"|\'[^'']*\'|\([^()]*\)/).length > 0 do - white_out_sql.scan(/\"[^""]*\"|\'[^'']*\'|\([^()]*\)/).each { |s| white_out_sql.gsub!(s,s.gsub(/./, '*')) } - end + def update( + where_filters: nil, + having_filters: nil, + sorts: nil, + page: nil, + per_page: nil + ) + @where_filters = where_filters if where_filters + @having_filters = having_filters if having_filters + @sorts = sorts if sorts + @page = page if page + @per_page = per_page if per_page + end + + private - @last_select_index = white_out_sql.rindex(/( |^)[Ss][Ee][Ll][Ee][Cc][Tt] /) + white_out_sql[/( |^)[Ss][Ee][Ll][Ee][Cc][Tt] /].size # space or new line at beginning of select, return index at the end of the word - @last_from_index = white_out_sql.index(/ [Ff][Rr][Oo][Mm] /, @last_select_index) - @last_where_index = white_out_sql.index(/ [Ww][Hh][Ee][Rr][Ee] /, @last_select_index) - @last_group_by_index = white_out_sql.index(/ [Gg][Rr][Oo][Uu][Pp] [Bb][Yy] /, @last_select_index) - @last_having_index = white_out_sql.index(/ [Hh][Aa][Vv][Ii][Nn][Gg] /, @last_select_index) - @last_order_by_index = white_out_sql.index(/ [Oo][Rr][Dd][Ee][Rr] [Bb][Yy ]/, @last_select_index) + def calculate_indexes + white_out_sql = self.class.white_out_query(@sql) - @where_included = !@last_where_index.nil? - @group_by_included = !@last_group_by_index.nil? - @having_included = !@last_having_index.nil? - @order_by_included = !@last_order_by_index.nil? + @where_included = !self.class.last_where_index(white_out_sql).nil? + @group_by_included = !self.class.last_group_by_index(white_out_sql).nil? + @having_included = !self.class.last_having_index(white_out_sql).nil? + @order_by_included = !self.class.last_order_by_index(white_out_sql).nil? - @insert_where_index = @last_group_by_index || @last_order_by_index || white_out_sql.length - @insert_having_index = @last_order_by_index || white_out_sql.length + @insert_where_index = self.class.last_group_by_index(white_out_sql) || self.class.last_order_by_index(white_out_sql) || white_out_sql.length + @insert_having_index = self.class.last_order_by_index(white_out_sql) || white_out_sql.length @insert_order_by_index = white_out_sql.length - @insert_join_index = @last_where_index || @last_group_by_index || @last_order_by_index || white_out_sql.length - @insert_select_index = @last_from_index - - # Determine alias expression combos. White out sql used in case there are any custom strings or subqueries in the select clause - white_out_select = white_out_sql[@last_select_index..@last_from_index] - select_clause = @sql[@last_select_index..@last_from_index] - comma_split_points = white_out_select.each_char.with_index.map{|char, i| i if char == ','}.compact - comma_split_points.unshift(-1) # We need the first select clause to start out with a 'split' - @alias_map = white_out_select.split(",").each_with_index.map do |x,i| - sql_alias = x.squish.split(" as ")[1] || x.squish.split(" AS ")[1] || x.squish.split(".")[1] # look for custom defined aliases or table.column notation - sql_alias = nil unless /^[a-zA-Z_]+$/.match?(sql_alias) # only allow aliases with letters and underscores - sql_expression = if x.split(" as ")[1] - expression_length = x.split(" as ")[0].length - select_clause[comma_split_points[i] + 1, expression_length] - elsif x.squish.split(" AS ")[1] - expression_length = x.split(" AS ")[0].length - select_clause[comma_split_points[i] + 1, expression_length] - elsif x.squish.split(".")[1] - select_clause[comma_split_points[i] + 1, x.length] - end - { - alias_name: sql_alias, - sql_expression: sql_expression.squish, - aggregate: /(array_agg|avg|bit_and|bit_or|bool_and|bool_or|count|every|json_agg|jsonb_agg|json_object_agg|jsonb_object_agg|max|min|string_agg|sum|xmlagg)\((.*)\)/.match?(sql_expression) - } - end - @alias_map.select!{|m| !m[:alias_name].nil?} + @insert_join_index = self.class.last_where_index(white_out_sql) || self.class.last_group_by_index(white_out_sql) || self.class.last_order_by_index(white_out_sql) || white_out_sql.length + @insert_select_index = self.class.last_from_index(white_out_sql) end def where_insert @@ -105,29 +135,6 @@ def total_count_select_insert " ,count(*) over () as _query_full_count " end - def build - modified_sql = @sql.dup - modified_sql = modified_sql.slice(0, @last_order_by_index) if @order_by_included # Remove previous sorting if it exists - modified_sql.insert(modified_sql.length, pagination_insert) if @page && @per_page - modified_sql.insert(@insert_order_by_index, sort_insert) if @sorts && @sorts.length > 0 - modified_sql.insert(@insert_having_index, having_insert) if @having_filters && @having_filters.length > 0 - modified_sql.insert(@insert_where_index, where_insert) if @where_filters && @where_filters.length > 0 - modified_sql.insert(@insert_select_index, total_count_select_insert) if @page && @per_page - modified_sql.squish - end - def update( - where_filters: nil, - having_filters: nil, - sorts: nil, - page: nil, - per_page: nil - ) - @where_filters = where_filters if where_filters - @having_filters = having_filters if having_filters - @sorts = sorts if sorts - @page = page if page - @per_page = per_page if per_page - end end end diff --git a/lib/pattern_query_helper/sql.rb b/lib/pattern_query_helper/sql.rb index e2d99fa..ea301c5 100644 --- a/lib/pattern_query_helper/sql.rb +++ b/lib/pattern_query_helper/sql.rb @@ -24,27 +24,19 @@ def initialize( @single_record = single_record @as_json_options = as_json_options - # Create the query string object - @query_string = PatternQueryHelper::QueryString.new( - sql: query, - page: @page, - per_page: @per_page - ) - - # Create our column maps. Use default maps if custom mappings aren't passed in. - @column_maps = PatternQueryHelper::ColumnMap.create_from_hash(column_mappings) - default_maps = @query_string.alias_map.map{|m| PatternQueryHelper::ColumnMap.new(**m)} - default_maps.each do |m| - @column_maps << m if @column_maps.select{|x| x.alias_name == m.alias_name}.empty? - end + # Create our column maps. + @column_maps = PatternQueryHelper::ColumnMap.create_column_mappings(custom_mappings: column_mappings, query: query) # Create the filter and sort objects @query_filter = PatternQueryHelper::QueryFilter.new(filter_values: filters, column_maps: @column_maps) @sorts = PatternQueryHelper::Sort.new(sort_string: sorts, column_maps: @column_maps) @associations = PatternQueryHelper::Associations.process_association_params(associations) - # Update the sql string with the filters and sorts - @query_string.update( + # create the query string object with the filters and sorts + @query_string = PatternQueryHelper::QueryString.new( + sql: query, + page: @page, + per_page: @per_page, where_filters: @query_filter.where_filter_strings, having_filters: @query_filter.having_filter_strings, sorts: @sorts.sort_strings, From d3172076aa500dc9f2741abd17977bcb429e00d7 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Thu, 27 Jun 2019 13:26:37 -0600 Subject: [PATCH 22/48] Readme update --- README.md | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b6c1998..7c3e5fe 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,40 @@ Or install it yourself as: ## Use +### SQL Queries + +#### Initialize + +To create a new sql query object run + +```ruby +PatternQueryHelper::Sql.new() +``` + +The following arguments are accepted when creating a new objects + +Argument | Required | Default Value | Description | Example Value | +--- | --- | --- | --- | --- | --- +model |
  • - [x]
| | the model to run the query against | | +query |
  • - [x]
| | the custom sql string to be executed | `select * from parents` | +query_params |
  • - [x]
| | a hash of bind variables to be embedded into the query | `{ age: 20, name: 'John' }` | + +```ruby +Argument Required DefaultValue Description +model: #required # the model to run the query against +query: #required # the custom sql to be executed +query_params: #optional # a list of bind variables to be embedded into the query +column_mappings: #optional # A hash that translates aliases to sql expressions +filters: #optional # a list of filters in the form of {"comparate_alias"=>{"operator_code"=>"value"}}. i.e. {"age"=>{"lt"=>100}, "children_count"=>{"gt"=>0}} +sorts: #optional # a comma separated string with a list of sort values i.e "age:desc,name:asc:lowercase" +page: #optional # define the page you want returned +per_page: #optional # define how many results you want per page +single_record: #optional #false # whether or not you expect the record to return a single result, if toggled, only the first result will be returned +associations: #optional # a list of activerecord associations you'd like included in the payload +as_json_options: #optional # a list of as_json options you'd like run before returning the payload +run: #optional #true # whether or not you'd like to run the query on initilization +``` + ### Active Record Queries To run an active record query execute @@ -33,12 +67,7 @@ query_helpers: See docs below valid_columns: Default is []. Pass in an array of columns you want to allow sorting and filtering on. single_record: Default is false. Pass in true to format payload as a single object instead of a list of objects -### Custom SQL Queries -To run a custom sql query execute -```ruby -PatternQueryHelper.run_sql_query(model, query, query_params, query_helpers, valid_columns, single_record) -``` model: A valid ActiveRecord model query: A string containing your custom SQL query query_params: a symbolized hash of binds to be included in your SQL query From 402af5cb55b0630bbbde5df5bdebd5d0b8f37e0b Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Thu, 27 Jun 2019 13:28:26 -0600 Subject: [PATCH 23/48] readme update --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7c3e5fe..dda69f4 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,11 @@ PatternQueryHelper::Sql.new() The following arguments are accepted when creating a new objects -Argument | Required | Default Value | Description | Example Value | +Argument | Required | Default Value | Description | Example Value --- | --- | --- | --- | --- | --- -model |
  • - [x]
| | the model to run the query against | | -query |
  • - [x]
| | the custom sql string to be executed | `select * from parents` | -query_params |
  • - [x]
| | a hash of bind variables to be embedded into the query | `{ age: 20, name: 'John' }` | +model |
  • - [x]
| | the model to run the query against | test +query |
  • - [x]
| | the custom sql string to be executed | `select * from parents` +query_params |
  • - [x]
| | a hash of bind variables to be embedded into the query | `{ age: 20, name: 'John' }` ```ruby Argument Required DefaultValue Description From fe62a07a17925a349e687b6192eced59247ed9ec Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Thu, 27 Jun 2019 13:29:38 -0600 Subject: [PATCH 24/48] readme update --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index dda69f4..bfacf1c 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,11 @@ model |
  • - [x]
| | the model to run the query against | test query |
  • - [x]
| | the custom sql string to be executed | `select * from parents` query_params |
  • - [x]
| | a hash of bind variables to be embedded into the query | `{ age: 20, name: 'John' }` +Markdown | Less | Pretty +--- | --- | --- +*Still* | `renders` | **nicely** +1 | 2 | 3 + ```ruby Argument Required DefaultValue Description model: #required # the model to run the query against From 0c021598a98ba45dd945e058d7bf98bdda06f721 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Thu, 27 Jun 2019 13:31:45 -0600 Subject: [PATCH 25/48] readme update --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bfacf1c..90f29f2 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,11 @@ PatternQueryHelper::Sql.new() The following arguments are accepted when creating a new objects Argument | Required | Default Value | Description | Example Value ---- | --- | --- | --- | --- | --- -model |
  • - [x]
| | the model to run the query against | test -query |
  • - [x]
| | the custom sql string to be executed | `select * from parents` -query_params |
  • - [x]
| | a hash of bind variables to be embedded into the query | `{ age: 20, name: 'John' }` +--- | --- | --- | --- | --- +model | - [x] | n/a | the model to run the query against | test + + + Markdown | Less | Pretty --- | --- | --- From ab53fe632e68636cea646fcc4b93147328bc0b21 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Thu, 27 Jun 2019 13:34:08 -0600 Subject: [PATCH 26/48] readme update --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 90f29f2..2bef858 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,15 @@ PatternQueryHelper::Sql.new() The following arguments are accepted when creating a new objects -Argument | Required | Default Value | Description | Example Value ---- | --- | --- | --- | --- -model | - [x] | n/a | the model to run the query against | test - +| Argument | Required | Default Value | Description | Example Value | +| --- | --- | --- | --- | --- | +| model |
  • - [x]
| n/a | the model to run the query against | | +| query |
  • - [x]
| the custom sql string to be executed | `select * from parents` | +model |
  • - [x]
| | the model to run the query against | test +query |
  • - [x]
| | the custom sql string to be executed | `select * from parents` +query_params |
  • - [x]
| | a hash of bind variables to be embedded into the query | `{ age: 20, name: 'John' }` Markdown | Less | Pretty --- | --- | --- From f640393defc86b42518dd299c7d783701c27eb83 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Thu, 27 Jun 2019 13:35:57 -0600 Subject: [PATCH 27/48] readme update --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2bef858..316fe6c 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ PatternQueryHelper::Sql.new() The following arguments are accepted when creating a new objects -| Argument | Required | Default Value | Description | Example Value | +| Argument | Description | Example Value | Required | Default Value | | --- | --- | --- | --- | --- | -| model |
  • - [x]
| n/a | the model to run the query against | | -| query |
  • - [x]
| the custom sql string to be executed | `select * from parents` | +| model | the model to run the query against | |
  • - [x]
| | +| query | the custom sql string to be executed | `select * from parents` |
  • - [x]
| | model |
  • - [x]
| | the model to run the query against | test From 5c74b96c4238acc8451310e390113262d2dd1e83 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Thu, 27 Jun 2019 13:49:06 -0600 Subject: [PATCH 28/48] readme update --- README.md | 58 +++++++++++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 316fe6c..e42cdaa 100644 --- a/README.md +++ b/README.md @@ -29,42 +29,40 @@ Or install it yourself as: To create a new sql query object run ```ruby -PatternQueryHelper::Sql.new() +PatternQueryHelper::Sql.new( + model:, # required + query:, # required + query_params: , # optional + column_mappings: , # optional + filters: , # optional + sorts: , # optional + page: , # optional + per_page: , # optional + single_record: , # optional, default: false + associations: , # optional + as_json_options: , # optional + run: # optional, default: true +) ``` The following arguments are accepted when creating a new objects -| Argument | Description | Example Value | Required | Default Value | -| --- | --- | --- | --- | --- | -| model | the model to run the query against | |
  • - [x]
| | -| query | the custom sql string to be executed | `select * from parents` |
  • - [x]
| | +| Argument | Description | Example Value | +| --- | --- | --- | +| model | the model to run the query against | +| query | the custom sql string to be executed | `select * from parents` | +| query_params | the custom sql string to be executed | `{ age: 20, name: 'John' }` | +| column_mappings | A hash that translates aliases to sql expressions | `{ "children_count" => { sql_expression: "count(children.id)", aggregate: true } }`| +| filters | a list of filters in the form of `{"comparate_alias"=>{"operator_code"=>"value"}}` | `{"age"=>{"lt"=>100}, "children_count"=>{"gt"=>0}}`| +| sorts | a comma separated string with a list of sort values | `age:desc,name:asc:lowercase`| +| page | define the page you want returned | 5 | +| page | define how many results you want per page | 20 | +| single_record | whether or not you expect the record to return a single result, if toggled, only the first result will be returned | | +| associations | a list of activerecord associations you'd like included in the payload | | +| as_json_options | a list of as_json options you'd like run before returning the payload | | +| as_json_options | whether or not you'd like to run the query on initilization | | -model |
  • - [x]
| | the model to run the query against | test -query |
  • - [x]
| | the custom sql string to be executed | `select * from parents` -query_params |
  • - [x]
| | a hash of bind variables to be embedded into the query | `{ age: 20, name: 'John' }` - -Markdown | Less | Pretty ---- | --- | --- -*Still* | `renders` | **nicely** -1 | 2 | 3 - -```ruby -Argument Required DefaultValue Description -model: #required # the model to run the query against -query: #required # the custom sql to be executed -query_params: #optional # a list of bind variables to be embedded into the query -column_mappings: #optional # A hash that translates aliases to sql expressions -filters: #optional # a list of filters in the form of {"comparate_alias"=>{"operator_code"=>"value"}}. i.e. {"age"=>{"lt"=>100}, "children_count"=>{"gt"=>0}} -sorts: #optional # a comma separated string with a list of sort values i.e "age:desc,name:asc:lowercase" -page: #optional # define the page you want returned -per_page: #optional # define how many results you want per page -single_record: #optional #false # whether or not you expect the record to return a single result, if toggled, only the first result will be returned -associations: #optional # a list of activerecord associations you'd like included in the payload -as_json_options: #optional # a list of as_json options you'd like run before returning the payload -run: #optional #true # whether or not you'd like to run the query on initilization -``` - ### Active Record Queries To run an active record query execute From 69354dfadcfdac7c23e7d6c5e0be5d39e80822b1 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Thu, 27 Jun 2019 14:27:10 -0600 Subject: [PATCH 29/48] readme update --- README.md | 139 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 124 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e42cdaa..0d19bed 100644 --- a/README.md +++ b/README.md @@ -47,21 +47,130 @@ PatternQueryHelper::Sql.new( The following arguments are accepted when creating a new objects -| Argument | Description | Example Value | -| --- | --- | --- | -| model | the model to run the query against | -| query | the custom sql string to be executed | `select * from parents` | -| query_params | the custom sql string to be executed | `{ age: 20, name: 'John' }` | -| column_mappings | A hash that translates aliases to sql expressions | `{ "children_count" => { sql_expression: "count(children.id)", aggregate: true } }`| -| filters | a list of filters in the form of `{"comparate_alias"=>{"operator_code"=>"value"}}` | `{"age"=>{"lt"=>100}, "children_count"=>{"gt"=>0}}`| -| sorts | a comma separated string with a list of sort values | `age:desc,name:asc:lowercase`| -| page | define the page you want returned | 5 | -| page | define how many results you want per page | 20 | -| single_record | whether or not you expect the record to return a single result, if toggled, only the first result will be returned | | -| associations | a list of activerecord associations you'd like included in the payload | | -| as_json_options | a list of as_json options you'd like run before returning the payload | | -| as_json_options | whether or not you'd like to run the query on initilization | | - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentDescriptionExample
modelthe model to run the query against + ```ruby + Parent + ``` +
querythe custom sql string to be executed + ```ruby + 'select * from parents' + ``` +
query_paramsa hash of bind variables to be embedded into the sql query + ```ruby + { + age: 20, + name: 'John' + } + ``` +
column_mappingsA hash that translates aliases to sql expressions + ```ruby + { + "age" => "parents.age" + "children_count" => { sql_expression: "count(children.id)", aggregate: true } + } + ``` +
filtersa list of filters in the form of `{"comparate_alias"=>{"operator_code"=>"value"}}` + ```ruby + { + "age" => { "lt" => 100 }, + "children_count" => { "gt" => 0 } + } + ``` +
sortsa comma separated string with a list of sort values + ```ruby + "age:desc,name:asc:lowercase" + ``` +
pagethe page you want returned + ```ruby + 5 + ``` +
per_pagethe number of results per page + ```ruby + 20 + ``` +
single_recordwhether or not you expect the record to return a single result, if toggled, only the first result will be returned + ```ruby + false + ``` +
associationsa list of activerecord associations you'd like included in the payload + ```ruby + + ``` +
as_json_optionsa list of as_json options you'd like run before returning the payload + ```ruby + + ``` +
runwhether or not you'd like to run the query on initilization + ```ruby + false + ``` +
### Active Record Queries From 3f16bf6b26c4ff8eaddaa52f2de10881a43cba15 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Thu, 27 Jun 2019 14:30:01 -0600 Subject: [PATCH 30/48] readme update --- README.md | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 0d19bed..3e53d66 100644 --- a/README.md +++ b/README.md @@ -57,117 +57,117 @@ The following arguments are accepted when creating a new objects model the model to run the query against - ```ruby +
         Parent
-      ```
+      
query the custom sql string to be executed - ```ruby +
         'select * from parents'
-      ```
+      
query_params a hash of bind variables to be embedded into the sql query - ```ruby +
         {
           age: 20,
           name: 'John'
         }
-      ```
+      
column_mappings A hash that translates aliases to sql expressions - ```ruby +
         {
           "age" => "parents.age"
           "children_count" => { sql_expression: "count(children.id)", aggregate: true }
         }
-      ```
+      
filters a list of filters in the form of `{"comparate_alias"=>{"operator_code"=>"value"}}` - ```ruby +
         {
           "age" => { "lt" => 100 },
           "children_count" => { "gt" => 0 }
         }
-      ```
+      
sorts a comma separated string with a list of sort values - ```ruby +
         "age:desc,name:asc:lowercase"
-      ```
+      
page the page you want returned - ```ruby +
         5
-      ```
+      
per_page the number of results per page - ```ruby +
         20
-      ```
+      
single_record whether or not you expect the record to return a single result, if toggled, only the first result will be returned - ```ruby +
         false
-      ```
+      
associations a list of activerecord associations you'd like included in the payload - ```ruby +
 
-      ```
+      
as_json_options a list of as_json options you'd like run before returning the payload - ```ruby +
 
-      ```
+      
run whether or not you'd like to run the query on initilization - ```ruby +
         false
-      ```
+      
From 592d080fc65ac8334b0c18d81a08053051c5ecfa Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Thu, 27 Jun 2019 14:31:19 -0600 Subject: [PATCH 31/48] readme update --- README.md | 244 +++++++++++++++++++++++++++--------------------------- 1 file changed, 122 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index 3e53d66..4a88c74 100644 --- a/README.md +++ b/README.md @@ -48,128 +48,128 @@ PatternQueryHelper::Sql.new( The following arguments are accepted when creating a new objects - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentDescriptionExample
modelthe model to run the query against -
-        Parent
-      
-
querythe custom sql string to be executed -
-        'select * from parents'
-      
-
query_paramsa hash of bind variables to be embedded into the sql query -
-        {
-          age: 20,
-          name: 'John'
-        }
-      
-
column_mappingsA hash that translates aliases to sql expressions -
-        {
-          "age" => "parents.age"
-          "children_count" => { sql_expression: "count(children.id)", aggregate: true }
-        }
-      
-
filtersa list of filters in the form of `{"comparate_alias"=>{"operator_code"=>"value"}}` -
-        {
-          "age" => { "lt" => 100 },
-          "children_count" => { "gt" => 0 }
-        }
-      
-
sortsa comma separated string with a list of sort values -
-        "age:desc,name:asc:lowercase"
-      
-
pagethe page you want returned -
-        5
-      
-
per_pagethe number of results per page -
-        20
-      
-
single_recordwhether or not you expect the record to return a single result, if toggled, only the first result will be returned -
-        false
-      
-
associationsa list of activerecord associations you'd like included in the payload -
-
-      
-
as_json_optionsa list of as_json options you'd like run before returning the payload -
-
-      
-
runwhether or not you'd like to run the query on initilization -
-        false
-      
-
ArgumentDescriptionExample
modelthe model to run the query against +
+Parent
+
+
querythe custom sql string to be executed +
+'select * from parents'
+
+
query_paramsa hash of bind variables to be embedded into the sql query +
+{
+  age: 20,
+  name: 'John'
+}
+
+
column_mappingsA hash that translates aliases to sql expressions +
+{
+  "age" => "parents.age"
+  "children_count" => { sql_expression: "count(children.id)", aggregate: true }
+}
+
+
filtersa list of filters in the form of `{"comparate_alias"=>{"operator_code"=>"value"}}` +
+{
+  "age" => { "lt" => 100 },
+  "children_count" => { "gt" => 0 }
+}
+
+
sortsa comma separated string with a list of sort values +
+"age:desc,name:asc:lowercase"
+
+
pagethe page you want returned +
+5
+
+
per_pagethe number of results per page +
+20
+
+
single_recordwhether or not you expect the record to return a single result, if toggled, only the first result will be returned +
+false
+
+
associationsa list of activerecord associations you'd like included in the payload +
+
+
+
as_json_optionsa list of as_json options you'd like run before returning the payload +
+
+
+
runwhether or not you'd like to run the query on initilization +
+false
+
+
### Active Record Queries From 6a9be715d09de820d3cd3d1ced2071f356242424 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Thu, 27 Jun 2019 14:37:21 -0600 Subject: [PATCH 32/48] readme update --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a88c74..59e74dc 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,10 @@ Parent
 {
   "age" => "parents.age"
-  "children_count" => { sql_expression: "count(children.id)", aggregate: true }
+  "children_count" => {
+    sql_expression: "count(children.id)",
+    aggregate: true
+  }
 }
 
From fe03ff3f7e28466f3e871f179f2d20038716a002 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Fri, 28 Jun 2019 13:53:45 -0600 Subject: [PATCH 33/48] renamed to QueryHelper --- Gemfile | 2 +- Gemfile.lock | 4 +- README.md | 20 ++-- bin/console | 2 +- lib/pattern_query_helper.rb | 19 --- lib/pattern_query_helper/version.rb | 3 - lib/query_helper.rb | 19 +++ .../active_record_query.rb | 2 +- .../associations.rb | 2 +- .../column_map.rb | 10 +- .../filter.rb | 2 +- lib/query_helper/query.rb | 111 ++++++++++++++++++ .../query_filter.rb | 4 +- .../query_helper_concern.rb | 2 +- .../query_string.rb | 2 +- .../sort.rb | 4 +- .../sql.rb | 14 +-- lib/query_helper/version.rb | 3 + ...ery_helper.gemspec => query_helper.gemspec | 8 +- spec/pattern_query_helper_spec.rb | 3 - .../associations_spec.rb | 8 +- .../column_map_spec.rb | 2 +- .../filter_spec.rb | 2 +- .../query_string_spec.rb | 2 +- .../sql_spec.rb | 2 +- spec/query_helper_spec.rb | 3 + spec/spec_helper.rb | 6 +- 27 files changed, 186 insertions(+), 75 deletions(-) delete mode 100644 lib/pattern_query_helper.rb delete mode 100644 lib/pattern_query_helper/version.rb create mode 100644 lib/query_helper.rb rename lib/{pattern_query_helper => query_helper}/active_record_query.rb (98%) rename lib/{pattern_query_helper => query_helper}/associations.rb (97%) rename lib/{pattern_query_helper => query_helper}/column_map.rb (88%) rename lib/{pattern_query_helper => query_helper}/filter.rb (99%) create mode 100644 lib/query_helper/query.rb rename lib/{pattern_query_helper => query_helper}/query_filter.rb (94%) rename lib/{pattern_query_helper => query_helper}/query_helper_concern.rb (95%) rename lib/{pattern_query_helper => query_helper}/query_string.rb (99%) rename lib/{pattern_query_helper => query_helper}/sort.rb (93%) rename lib/{pattern_query_helper => query_helper}/sql.rb (86%) create mode 100644 lib/query_helper/version.rb rename pattern_query_helper.gemspec => query_helper.gemspec (90%) delete mode 100644 spec/pattern_query_helper_spec.rb rename spec/{pattern_query_helper => query_helper}/associations_spec.rb (74%) rename spec/{pattern_query_helper => query_helper}/column_map_spec.rb (94%) rename spec/{pattern_query_helper => query_helper}/filter_spec.rb (98%) rename spec/{pattern_query_helper => query_helper}/query_string_spec.rb (92%) rename spec/{pattern_query_helper => query_helper}/sql_spec.rb (96%) create mode 100644 spec/query_helper_spec.rb diff --git a/Gemfile b/Gemfile index 6b5dbaa..ba44849 100644 --- a/Gemfile +++ b/Gemfile @@ -2,5 +2,5 @@ source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } -# Specify your gem's dependencies in pattern_query_helper.gemspec +# Specify your gem's dependencies in query_helper.gemspec gemspec diff --git a/Gemfile.lock b/Gemfile.lock index 32a1ab0..5b39d0c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - pattern_query_helper (0.2.10) + query_helper (0.2.10) activerecord (~> 5.0) activesupport (~> 5.0) @@ -54,7 +54,7 @@ DEPENDENCIES bundler (~> 1.16) byebug faker (~> 1.9.3) - pattern_query_helper! + query_helper! rake (~> 10.0) rspec (~> 3.0) sqlite3 (~> 1.3.6) diff --git a/README.md b/README.md index 59e74dc..8e309eb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# PatternQueryHelper -[![TravisCI](https://travis-ci.org/iserve-products/pattern_query_helper.svg?branch=master)](https://travis-ci.org/iserve-products/pattern_query_helper) -[![Gem Version](https://badge.fury.io/rb/pattern_query_helper.svg)](https://badge.fury.io/rb/pattern_query_helper) +# QueryHelper +[![TravisCI](https://travis-ci.org/iserve-products/query_helper.svg?branch=master)](https://travis-ci.org/iserve-products/query_helper) +[![Gem Version](https://badge.fury.io/rb/query_helper.svg)](https://badge.fury.io/rb/query_helper) Ruby Gem developed and used at Pattern to paginate, sort, filter, and include associations on sql and active record queries. @@ -9,7 +9,7 @@ Ruby Gem developed and used at Pattern to paginate, sort, filter, and include as Add this line to your application's Gemfile: ```ruby -gem 'pattern_query_helper' +gem 'query_helper' ``` And then execute: @@ -18,7 +18,7 @@ And then execute: Or install it yourself as: - $ gem install pattern_query_helper + $ gem install query_helper ## Use @@ -29,7 +29,7 @@ Or install it yourself as: To create a new sql query object run ```ruby -PatternQueryHelper::Sql.new( +QueryHelper::Sql.new( model:, # required query:, # required query_params: , # optional @@ -179,7 +179,7 @@ false To run an active record query execute ```ruby -PatternQueryHelper.run_active_record_query(active_record_call, query_helpers, valid_columns, single_record) +QueryHelper.run_active_record_query(active_record_call, query_helpers, valid_columns, single_record) ``` active_record_call: Valid active record syntax (i.e. ```Object.where(state: 'Active')```) query_helpers: See docs below @@ -291,7 +291,7 @@ query_helpers = { ## Payload Formats -The PatternQueryHelper gem will return results in one of three formats +The QueryHelper gem will return results in one of three formats ### Paginated List Payload ```json @@ -370,7 +370,7 @@ The PatternQueryHelper gem will return results in one of three formats ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/pattern_query_helper. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. +Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/query_helper. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. ## License @@ -378,4 +378,4 @@ The gem is available as open source under the terms of the [MIT License](https:/ ## Code of Conduct -Everyone interacting in the PatternQueryHelper project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/pattern_query_helper/blob/master/CODE_OF_CONDUCT.md). +Everyone interacting in the QueryHelper project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/query_helper/blob/master/CODE_OF_CONDUCT.md). diff --git a/bin/console b/bin/console index 57136d3..9492f54 100755 --- a/bin/console +++ b/bin/console @@ -1,7 +1,7 @@ #!/usr/bin/env ruby require "bundler/setup" -require "pattern_query_helper" +require "query_helper" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. diff --git a/lib/pattern_query_helper.rb b/lib/pattern_query_helper.rb deleted file mode 100644 index b6609f7..0000000 --- a/lib/pattern_query_helper.rb +++ /dev/null @@ -1,19 +0,0 @@ -require "active_record" - -require "pattern_query_helper/version" -require "pattern_query_helper/filter" -require "pattern_query_helper/column_map" -require "pattern_query_helper/query_string" -require "pattern_query_helper/sql" -require "pattern_query_helper/active_record_query" -require "pattern_query_helper/query_filter" -require "pattern_query_helper/sort" -require "pattern_query_helper/associations" -require "pattern_query_helper/query_helper_concern" - -module PatternQueryHelper - - class << self - attr_accessor :active_record_adapter - end -end diff --git a/lib/pattern_query_helper/version.rb b/lib/pattern_query_helper/version.rb deleted file mode 100644 index 1c65dd5..0000000 --- a/lib/pattern_query_helper/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module PatternQueryHelper - VERSION = "0.2.10" -end diff --git a/lib/query_helper.rb b/lib/query_helper.rb new file mode 100644 index 0000000..85e2464 --- /dev/null +++ b/lib/query_helper.rb @@ -0,0 +1,19 @@ +require "active_record" + +require "query_helper/version" +require "query_helper/filter" +require "query_helper/column_map" +require "query_helper/query_string" +require "query_helper/sql" +require "query_helper/active_record_query" +require "query_helper/query_filter" +require "query_helper/sort" +require "query_helper/associations" +require "query_helper/query_helper_concern" + +module QueryHelper + + class << self + attr_accessor :active_record_adapter + end +end diff --git a/lib/pattern_query_helper/active_record_query.rb b/lib/query_helper/active_record_query.rb similarity index 98% rename from lib/pattern_query_helper/active_record_query.rb rename to lib/query_helper/active_record_query.rb index b3f0f3e..78ab1a3 100644 --- a/lib/pattern_query_helper/active_record_query.rb +++ b/lib/query_helper/active_record_query.rb @@ -1,4 +1,4 @@ -module PatternQueryHelper +module QueryHelper class ActiveRecordQuery < Sql def initialize( diff --git a/lib/pattern_query_helper/associations.rb b/lib/query_helper/associations.rb similarity index 97% rename from lib/pattern_query_helper/associations.rb rename to lib/query_helper/associations.rb index eddd26f..8226a31 100644 --- a/lib/pattern_query_helper/associations.rb +++ b/lib/query_helper/associations.rb @@ -1,4 +1,4 @@ -module PatternQueryHelper +module QueryHelper class Associations def self.process_association_params(associations) associations ||= [] diff --git a/lib/pattern_query_helper/column_map.rb b/lib/query_helper/column_map.rb similarity index 88% rename from lib/pattern_query_helper/column_map.rb rename to lib/query_helper/column_map.rb index 419240a..13577be 100644 --- a/lib/pattern_query_helper/column_map.rb +++ b/lib/query_helper/column_map.rb @@ -1,4 +1,4 @@ -module PatternQueryHelper +module QueryHelper class ColumnMap def self.create_column_mappings(custom_mappings:, query:) @@ -35,9 +35,9 @@ def self.create_from_hash(hash) def self.find_aliases_in_query(query) # Determine alias expression combos. White out sql used in case there # are any custom strings or subqueries in the select clause - select_index = PatternQueryHelper::QueryString.last_select_index(query) - from_index = PatternQueryHelper::QueryString.last_from_index(query) - white_out_select = PatternQueryHelper::QueryString.white_out_query(query)[select_index..from_index] + select_index = QueryHelper::QueryString.last_select_index(query) + from_index = QueryHelper::QueryString.last_from_index(query) + white_out_select = QueryHelper::QueryString.white_out_query(query)[select_index..from_index] select_clause = query[select_index..from_index] comma_split_points = white_out_select.each_char.with_index.map{|char, i| i if char == ','}.compact comma_split_points.unshift(-1) # We need the first select clause to start out with a 'split' @@ -53,7 +53,7 @@ def self.find_aliases_in_query(query) elsif x.squish.split(".")[1] select_clause[comma_split_points[i] + 1, x.length] end - PatternQueryHelper::ColumnMap.new( + QueryHelper::ColumnMap.new( alias_name: sql_alias, sql_expression: sql_expression.squish, aggregate: /(array_agg|avg|bit_and|bit_or|bool_and|bool_or|count|every|json_agg|jsonb_agg|json_object_agg|jsonb_object_agg|max|min|string_agg|sum|xmlagg)\((.*)\)/.match?(sql_expression) diff --git a/lib/pattern_query_helper/filter.rb b/lib/query_helper/filter.rb similarity index 99% rename from lib/pattern_query_helper/filter.rb rename to lib/query_helper/filter.rb index 4f58bc5..35c0885 100644 --- a/lib/pattern_query_helper/filter.rb +++ b/lib/query_helper/filter.rb @@ -1,4 +1,4 @@ -module PatternQueryHelper +module QueryHelper class Filter attr_accessor :operator, :criterion, :comparate, :operator_code, :aggregate, :bind_variable diff --git a/lib/query_helper/query.rb b/lib/query_helper/query.rb new file mode 100644 index 0000000..084f04c --- /dev/null +++ b/lib/query_helper/query.rb @@ -0,0 +1,111 @@ +module QueryHelper + class Sql + + attr_accessor :model, :query_string, :query_params, :query_filter, :results + + def initialize( + model:, # the model to run the query against + query_string:, # a query string object + query_params: {}, # a list of bind variables to be embedded into the query + query_filter: nil, # a QueryFilter object + sort: nil, # a Sort object + page: nil, # define the page you want returned + per_page: nil, # define how many results you want per page + single_record: false, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned + associations: [], # a list of activerecord associations you'd like included in the payload + as_json_options: {}, # a list of as_json options you'd like run before returning the payload + run: true # whether or not you'd like to run the query on initilization + ) + @model = model + @query_params = query_params + @query_filter = query_filter + @sort = sort + @page = page.to_i if page + @per_page = per_page.to_i if per_page + @single_record = single_record + @associations = associations + @as_json_options = as_json_options + + # Create the filter and sort objects + @query_filter = QueryHelper::QueryFilter.new(filter_values: filters, column_maps: @column_maps) + @sorts = QueryHelper::Sort.new(sort_string: sorts, column_maps: @column_maps) + @associations = QueryHelper::Associations.process_association_params(associations) + + # create the query string object with the filters and sorts + @query_string = QueryHelper::QueryString.new( + sql: query, + page: @page, + per_page: @per_page, + where_filters: @query_filter.where_filter_strings, + having_filters: @query_filter.having_filter_strings, + sorts: @sorts.sort_strings, + ) + + # Merge the filter bind variables into the query_params + @query_params.merge!(@query_filter.bind_variables) + + execute_query() if run + end + + def execute_query + # Execute Sql Query + @results = @model.find_by_sql([@query_string.build(), @query_params]) # Execute Sql Query + @results = @results.first if @single_record # Return a single result if requested + + determine_count() + load_associations() + clean_results() + end + + def payload + if @page && @per_page + { + pagination: pagination_results(), + data: @results + } + else + { data: @results } + end + end + + private + + def pagination_results + total_pages = (@count/(@per_page.nonzero? || 1).to_f).ceil + next_page = @page + 1 if @page.between?(1, total_pages - 1) + previous_page = @page - 1 if @page.between?(2, total_pages) + first_page = @page == 1 + last_page = @page == total_pages + out_of_range = !@page.between?(1,total_pages) + + { + count: @count, + current_page: @page, + next_page: next_page, + previous_page: previous_page, + total_pages: total_pages, + per_page: @per_page, + first_page: first_page, + last_page: last_page, + out_of_range: out_of_range + } + end + + def clean_results + @results.map!{ |r| r.except("_query_full_count") } if @page && @per_page + end + + def load_associations + @results = QueryHelper::Associations.load_associations( + payload: @results, + associations: @associations, + as_json_options: @as_json_options + ) + end + + def determine_count + # Determine total result count (unpaginated) + @count = @page && @per_page && results.length > 0 ? results.first["_query_full_count"] : results.length + end + end +end diff --git a/lib/pattern_query_helper/query_filter.rb b/lib/query_helper/query_filter.rb similarity index 94% rename from lib/pattern_query_helper/query_filter.rb rename to lib/query_helper/query_filter.rb index a728abe..b4dbeab 100644 --- a/lib/pattern_query_helper/query_filter.rb +++ b/lib/query_helper/query_filter.rb @@ -1,4 +1,4 @@ -module PatternQueryHelper +module QueryHelper class QueryFilter attr_accessor :filters, :where_filter_strings, :having_filter_strings, :bind_variables @@ -30,7 +30,7 @@ def create_filters criterion = criteria.values.first # create the filter - filters << PatternQueryHelper::Filter.new( + filters << QueryHelper::Filter.new( operator_code: operator_code, criterion: criterion, comparate: comparate, diff --git a/lib/pattern_query_helper/query_helper_concern.rb b/lib/query_helper/query_helper_concern.rb similarity index 95% rename from lib/pattern_query_helper/query_helper_concern.rb rename to lib/query_helper/query_helper_concern.rb index cb4d021..04c4186 100644 --- a/lib/pattern_query_helper/query_helper_concern.rb +++ b/lib/query_helper/query_helper_concern.rb @@ -1,6 +1,6 @@ require 'active_support/concern' -module PatternQueryHelper +module QueryHelper module QueryHelperConcern extend ActiveSupport::Concern diff --git a/lib/pattern_query_helper/query_string.rb b/lib/query_helper/query_string.rb similarity index 99% rename from lib/pattern_query_helper/query_string.rb rename to lib/query_helper/query_string.rb index 172b953..3274c7a 100644 --- a/lib/pattern_query_helper/query_string.rb +++ b/lib/query_helper/query_string.rb @@ -1,4 +1,4 @@ -module PatternQueryHelper +module QueryHelper class QueryString attr_accessor :query_string, :where_filters, :having_filters, :sorts, :page, :per_page, :alias_map diff --git a/lib/pattern_query_helper/sort.rb b/lib/query_helper/sort.rb similarity index 93% rename from lib/pattern_query_helper/sort.rb rename to lib/query_helper/sort.rb index 158fb4a..ae0fa85 100644 --- a/lib/pattern_query_helper/sort.rb +++ b/lib/query_helper/sort.rb @@ -1,4 +1,4 @@ -module PatternQueryHelper +module QueryHelper class Sort attr_accessor :sort_strings @@ -24,7 +24,7 @@ def parse_sort_string end if direction == "desc" - case PatternQueryHelper.active_record_adapter + case QueryHelper.active_record_adapter when "sqlite3" # sqlite3 is used in the test suite direction = "desc" else diff --git a/lib/pattern_query_helper/sql.rb b/lib/query_helper/sql.rb similarity index 86% rename from lib/pattern_query_helper/sql.rb rename to lib/query_helper/sql.rb index ea301c5..f6f224a 100644 --- a/lib/pattern_query_helper/sql.rb +++ b/lib/query_helper/sql.rb @@ -1,4 +1,4 @@ -module PatternQueryHelper +module QueryHelper class Sql attr_accessor :model, :query_string, :query_params, :query_filter, :results @@ -25,15 +25,15 @@ def initialize( @as_json_options = as_json_options # Create our column maps. - @column_maps = PatternQueryHelper::ColumnMap.create_column_mappings(custom_mappings: column_mappings, query: query) + @column_maps = QueryHelper::ColumnMap.create_column_mappings(custom_mappings: column_mappings, query: query) # Create the filter and sort objects - @query_filter = PatternQueryHelper::QueryFilter.new(filter_values: filters, column_maps: @column_maps) - @sorts = PatternQueryHelper::Sort.new(sort_string: sorts, column_maps: @column_maps) - @associations = PatternQueryHelper::Associations.process_association_params(associations) + @query_filter = QueryHelper::QueryFilter.new(filter_values: filters, column_maps: @column_maps) + @sorts = QueryHelper::Sort.new(sort_string: sorts, column_maps: @column_maps) + @associations = QueryHelper::Associations.process_association_params(associations) # create the query string object with the filters and sorts - @query_string = PatternQueryHelper::QueryString.new( + @query_string = QueryHelper::QueryString.new( sql: query, page: @page, per_page: @per_page, @@ -97,7 +97,7 @@ def clean_results end def load_associations - @results = PatternQueryHelper::Associations.load_associations( + @results = QueryHelper::Associations.load_associations( payload: @results, associations: @associations, as_json_options: @as_json_options diff --git a/lib/query_helper/version.rb b/lib/query_helper/version.rb new file mode 100644 index 0000000..b9e3970 --- /dev/null +++ b/lib/query_helper/version.rb @@ -0,0 +1,3 @@ +module QueryHelper + VERSION = "0.2.10" +end diff --git a/pattern_query_helper.gemspec b/query_helper.gemspec similarity index 90% rename from pattern_query_helper.gemspec rename to query_helper.gemspec index b706034..233955e 100644 --- a/pattern_query_helper.gemspec +++ b/query_helper.gemspec @@ -1,17 +1,17 @@ lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require "pattern_query_helper/version" +require "query_helper/version" Gem::Specification.new do |spec| - spec.name = "pattern_query_helper" - spec.version = PatternQueryHelper::VERSION + spec.name = "query_helper" + spec.version = QueryHelper::VERSION spec.authors = ["Evan McDaniel"] spec.email = ["eamigo13@gmail.com"] spec.summary = %q{Ruby Gem to help with pagination and data formatting at Pattern, Inc.} spec.description = %q{Ruby gem developed to help with pagination, filtering, sorting, and including associations on both active record queries and custom sql queries} - spec.homepage = "https://github.com/iserve-products/pattern_query_helper" + spec.homepage = "https://github.com/iserve-products/query_helper" spec.license = "MIT" # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' diff --git a/spec/pattern_query_helper_spec.rb b/spec/pattern_query_helper_spec.rb deleted file mode 100644 index 484155a..0000000 --- a/spec/pattern_query_helper_spec.rb +++ /dev/null @@ -1,3 +0,0 @@ -RSpec.describe PatternQueryHelper do - it "pending tests" -end diff --git a/spec/pattern_query_helper/associations_spec.rb b/spec/query_helper/associations_spec.rb similarity index 74% rename from spec/pattern_query_helper/associations_spec.rb rename to spec/query_helper/associations_spec.rb index 9d47265..d1aae4d 100644 --- a/spec/pattern_query_helper/associations_spec.rb +++ b/spec/query_helper/associations_spec.rb @@ -1,19 +1,19 @@ require "spec_helper" -RSpec.describe PatternQueryHelper::Associations do +RSpec.describe QueryHelper::Associations do describe "process_association_params" do it "parses association params" do - associations = PatternQueryHelper::Associations.process_association_params("parent") + associations = QueryHelper::Associations.process_association_params("parent") expect(associations).to eq([:parent]) end end describe "load_associations" do it "loads associations" do - associations = PatternQueryHelper::Associations.process_association_params("parent") + associations = QueryHelper::Associations.process_association_params("parent") payload = Child.all - results = PatternQueryHelper::Associations.load_associations(payload: payload, associations: associations) + results = QueryHelper::Associations.load_associations(payload: payload, associations: associations) results.each do |child| expect(child["parent_id"]).to eq(child["parent"]["id"]) end diff --git a/spec/pattern_query_helper/column_map_spec.rb b/spec/query_helper/column_map_spec.rb similarity index 94% rename from spec/pattern_query_helper/column_map_spec.rb rename to spec/query_helper/column_map_spec.rb index ce1d01c..7a05a48 100644 --- a/spec/pattern_query_helper/column_map_spec.rb +++ b/spec/query_helper/column_map_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -RSpec.describe PatternQueryHelper::ColumnMap do +RSpec.describe QueryHelper::ColumnMap do let(:valid_operator_codes) {["gte", "lte", "gt", "lt", "eql", "noteql", "in", "notin", "null"]} describe ".create_from_hash" do diff --git a/spec/pattern_query_helper/filter_spec.rb b/spec/query_helper/filter_spec.rb similarity index 98% rename from spec/pattern_query_helper/filter_spec.rb rename to spec/query_helper/filter_spec.rb index e100f7d..5321d29 100644 --- a/spec/pattern_query_helper/filter_spec.rb +++ b/spec/query_helper/filter_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -RSpec.describe PatternQueryHelper::Filter do +RSpec.describe QueryHelper::Filter do let(:valid_operator_codes) {["gte", "lte", "gt", "lt", "eql", "noteql", "in", "notin", "null"]} describe ".sql_string" do diff --git a/spec/pattern_query_helper/query_string_spec.rb b/spec/query_helper/query_string_spec.rb similarity index 92% rename from spec/pattern_query_helper/query_string_spec.rb rename to spec/query_helper/query_string_spec.rb index 1788a75..2f2cae0 100644 --- a/spec/pattern_query_helper/query_string_spec.rb +++ b/spec/query_helper/query_string_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -RSpec.describe PatternQueryHelper::QueryString do +RSpec.describe QueryHelper::QueryString do let(:complex_query) do query = %{ with cte as ( diff --git a/spec/pattern_query_helper/sql_spec.rb b/spec/query_helper/sql_spec.rb similarity index 96% rename from spec/pattern_query_helper/sql_spec.rb rename to spec/query_helper/sql_spec.rb index f299b01..47eaaeb 100644 --- a/spec/pattern_query_helper/sql_spec.rb +++ b/spec/query_helper/sql_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -RSpec.describe PatternQueryHelper::Sql do +RSpec.describe QueryHelper::Sql do let(:query) do %{ select parents.id, parents.name, parents.age, count(children.id) as children_count diff --git a/spec/query_helper_spec.rb b/spec/query_helper_spec.rb new file mode 100644 index 0000000..40d3bc2 --- /dev/null +++ b/spec/query_helper_spec.rb @@ -0,0 +1,3 @@ +RSpec.describe QueryHelper do + it "pending tests" +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3a13acc..f22b710 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,5 @@ require "bundler/setup" -require "pattern_query_helper" +require "query_helper" require 'sqlite3' require 'active_record' require 'faker' @@ -36,11 +36,11 @@ } end - PatternQueryHelper.active_record_adapter = "sqlite3" + QueryHelper.active_record_adapter = "sqlite3" # Set up a database that resides in RAM ActiveRecord::Base.establish_connection( - adapter: PatternQueryHelper.active_record_adapter, + adapter: QueryHelper.active_record_adapter, database: ':memory:' ) From c01e7a147f7968b2829322fccea1b9bdb28f9f8c Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Sat, 29 Jun 2019 07:35:42 -0600 Subject: [PATCH 34/48] SqlManipulator and SqlParser --- lib/query_helper.rb | 2 + lib/query_helper/sql_manipulator.rb | 68 ++++++++++++ lib/query_helper/sql_parser.rb | 158 ++++++++++++++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 lib/query_helper/sql_manipulator.rb create mode 100644 lib/query_helper/sql_parser.rb diff --git a/lib/query_helper.rb b/lib/query_helper.rb index 85e2464..32dd78f 100644 --- a/lib/query_helper.rb +++ b/lib/query_helper.rb @@ -10,6 +10,8 @@ require "query_helper/sort" require "query_helper/associations" require "query_helper/query_helper_concern" +require "query_helper/sql_parser" +require "query_helper/sql_manipulator" module QueryHelper diff --git a/lib/query_helper/sql_manipulator.rb b/lib/query_helper/sql_manipulator.rb new file mode 100644 index 0000000..3542501 --- /dev/null +++ b/lib/query_helper/sql_manipulator.rb @@ -0,0 +1,68 @@ +require "query_helper/sql_parser" + +module QueryHelper + class SqlManipulator + + attr_accessor :sql + + def initialize( + sql:, + where_clauses: nil, + having_clauses: nil, + order_by_clauses: nil, + include_limit_clause: false + ) + @parser = SqlParser.new(sql) + @sql = @parser.sql + @where_clauses = where_clauses + @having_clauses = having_clauses + @order_by_clauses = order_by_clauses + @include_limit_clause = include_limit_clause + build() + end + + def build + insert_limit_clause() + insert_order_by_clause() + insert_having_clauses() + insert_where_clauses() + insert_total_count_select_clause() + end + + private + + def insert_total_count_select_clause + return unless @include_limit_clause + total_count_clause = " ,count(*) over () as _query_full_count " + @sql.insert(@parser.insert_select_index, total_count_clause) + # Potentially update parser here + end + + def insert_where_clauses + return unless @where_clauses + begin_string = @parser.where_included ? "and" : "where" + filter_string = @where_clauses.join(" and ") + " #{begin_string} #{filter_string} " + @sql.insert(@parser.insert_where_index, " #{begin_string} #{filter_string} ") + end + + def insert_having_clauses + return unless @having_clauses + begin_string = @parser.having_included ? "and" : "having" + filter_string = @having_clauses.join(" and ") + @sql.insert(@parser.insert_having_index, " #{begin_string} #{filter_string} ") + end + + def insert_order_by_clause + return unless @order_by_clauses + @sql.slice!(@parser.order_by_clause) if @parser.order_by_included? # remove existing order by clause + @sql.insert(@parser.insert_having_index, " order by #{@order_by_clauses.join(", ")} ") + end + + def insert_limit_clause + return unless @include_limit_clause + @sql.slice!(@parser.limit_clause) if @parser.limit_included? # remove existing limit clause + @sql.insert(@parser.insert_limit_index, " limit :limit offset :offset ") + end + end +end diff --git a/lib/query_helper/sql_parser.rb b/lib/query_helper/sql_parser.rb new file mode 100644 index 0000000..2bd3ee3 --- /dev/null +++ b/lib/query_helper/sql_parser.rb @@ -0,0 +1,158 @@ +module QueryHelper + class SqlParser + + attr_accessor :sql + + def initialize(sql) + update(sql) + end + + def update(sql) + @sql = sql + remove_comments() + white_out() + end + + def remove_comments + # Remove SQL inline comments (/* */) and line comments (--) + @sql = @sql.gsub(/\/\*(.*?)\*\//, '').gsub(/--(.*)$/, '') + @sql.squish! + end + + def white_out + # Replace everything between () and '' and "" + # This will allow us to ignore subqueries, common table expressions, + # regex, custom strings, etc. when determining injection points + # and performing other manipulations + @white_out_sql = @sql.dup + while @white_out_sql.scan(/\"[^""]*\"|\'[^'']*\'|\([^()]*\)/).length > 0 do + @white_out_sql.scan(/\"[^""]*\"|\'[^'']*\'|\([^()]*\)/).each { |s| @white_out_sql.gsub!(s,s.gsub(/./, '*')) } + end + end + + def select_index(position=:start) + regex = /( |^)[Ss][Ee][Ll][Ee][Cc][Tt] / # space or new line at beginning of select + find_index(regex, position) + end + + def from_index(position=:start) + regex = / [Ff][Rr][Oo][Mm] / + find_index(regex, position) + end + + def where_index(position=:start) + regex = / [Ww][Hh][Ee][Rr][Ee] / + find_index(regex, position) + end + + def group_by_index(position=:start) + regex = / [Gg][Rr][Oo][Uu][Pp] [Bb][Yy] / + find_index(regex, position) + end + + def having_index(position=:start) + regex = / [Hh][Aa][Vv][Ii][Nn][Gg] / + find_index(regex, position) + end + + def order_by_index(position=:start) + regex = / [Oo][Rr][Dd][Ee][Rr] [Bb][Yy] / + find_index(regex, position) + end + + def limit_index(position=:start) + regex = / [Ll][Ii][Mm][Ii][Tt] / + find_index(regex, position) + end + + def select_included? + !select_index.nil? + end + + def from_included? + !from_index.nil? + end + + def where_included? + !where_index.nil? + end + + def group_by_included? + !group_by_index.nil? + end + + def having_included? + !having_index.nil? + end + + def order_by_included? + !order_by_index.nil? + end + + def limit_included? + !limit_index.nil? + end + + def insert_select_index + from_index() || where_index() || group_by_index() || order_by_index() || limit_index() || @sql.length + end + + def insert_join_index + where_index() || group_by_index() || order_by_index() || limit_index() || @sql.length + end + + def insert_where_index + group_by_index() || order_by_index() || limit_index() || @sql.length + end + + def insert_having_index + # raise ArgumentError.new("Cannot calculate insert_having_index because the query has no group by clause") unless group_by_included? + order_by_index() || limit_index() || @sql.length + end + + def insert_order_by_index + # ArgumentError.new("This query already includes an order by clause") if order_by_included? + limit_index() || @sql.length + end + + def insert_limit_index + # ArgumentError.new("This query already includes a limit clause") if limit_included? + @sql.length + end + + def select_clause + @sql[select_index()..insert_select_index()] if select_included? + end + + def from_clause + @sql[from_index()..insert_join_index()] if from_included? + end + + def where_clause + @sql[where_index()..insert_where_index()] if where_included? + end + + # def group_by_clause + # @sql[group_by_index()..insert_group_by_index()] if group_by_included? + # end + + def having_clause + @sql[having_index()..insert_having_index()] if having_included? + end + + def order_by_clause + @sql[order_by_index()..insert_order_by_index()] if order_by_included? + end + + def limit_clause + @sql[limit_index()..insert_limit_index()] if limit_included? + end + + private + + def find_index(regex, position=:start) + start_position = @white_out_sql.rindex(regex) + return position == :start ? start_position : start_position + @white_out_sql[regex].size() + end + end +end From 6db1e8c6e988ef1300943a2a13e633b57a601b06 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Sun, 30 Jun 2019 07:25:46 -0600 Subject: [PATCH 35/48] WIP --- lib/query_helper/column_map.rb | 2 + lib/query_helper/query.rb | 111 ----------------- lib/query_helper/query_helper_concern.rb | 8 ++ .../{query_filter.rb => sql_filter.rb} | 2 +- lib/query_helper/sql_manipulator.rb | 1 - lib/query_helper/sql_parser.rb | 30 +++++ lib/query_helper/sql_runner.rb | 116 ++++++++++++++++++ lib/query_helper/{sort.rb => sql_sort.rb} | 2 +- 8 files changed, 158 insertions(+), 114 deletions(-) delete mode 100644 lib/query_helper/query.rb rename lib/query_helper/{query_filter.rb => sql_filter.rb} (98%) create mode 100644 lib/query_helper/sql_runner.rb rename lib/query_helper/{sort.rb => sql_sort.rb} (98%) diff --git a/lib/query_helper/column_map.rb b/lib/query_helper/column_map.rb index 13577be..7c5a4c1 100644 --- a/lib/query_helper/column_map.rb +++ b/lib/query_helper/column_map.rb @@ -1,3 +1,5 @@ +require "query_helper/sql_parser" + module QueryHelper class ColumnMap diff --git a/lib/query_helper/query.rb b/lib/query_helper/query.rb deleted file mode 100644 index 084f04c..0000000 --- a/lib/query_helper/query.rb +++ /dev/null @@ -1,111 +0,0 @@ -module QueryHelper - class Sql - - attr_accessor :model, :query_string, :query_params, :query_filter, :results - - def initialize( - model:, # the model to run the query against - query_string:, # a query string object - query_params: {}, # a list of bind variables to be embedded into the query - query_filter: nil, # a QueryFilter object - sort: nil, # a Sort object - page: nil, # define the page you want returned - per_page: nil, # define how many results you want per page - single_record: false, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned - associations: [], # a list of activerecord associations you'd like included in the payload - as_json_options: {}, # a list of as_json options you'd like run before returning the payload - run: true # whether or not you'd like to run the query on initilization - ) - @model = model - @query_params = query_params - @query_filter = query_filter - @sort = sort - @page = page.to_i if page - @per_page = per_page.to_i if per_page - @single_record = single_record - @associations = associations - @as_json_options = as_json_options - - # Create the filter and sort objects - @query_filter = QueryHelper::QueryFilter.new(filter_values: filters, column_maps: @column_maps) - @sorts = QueryHelper::Sort.new(sort_string: sorts, column_maps: @column_maps) - @associations = QueryHelper::Associations.process_association_params(associations) - - # create the query string object with the filters and sorts - @query_string = QueryHelper::QueryString.new( - sql: query, - page: @page, - per_page: @per_page, - where_filters: @query_filter.where_filter_strings, - having_filters: @query_filter.having_filter_strings, - sorts: @sorts.sort_strings, - ) - - # Merge the filter bind variables into the query_params - @query_params.merge!(@query_filter.bind_variables) - - execute_query() if run - end - - def execute_query - # Execute Sql Query - @results = @model.find_by_sql([@query_string.build(), @query_params]) # Execute Sql Query - @results = @results.first if @single_record # Return a single result if requested - - determine_count() - load_associations() - clean_results() - end - - def payload - if @page && @per_page - { - pagination: pagination_results(), - data: @results - } - else - { data: @results } - end - end - - private - - def pagination_results - total_pages = (@count/(@per_page.nonzero? || 1).to_f).ceil - next_page = @page + 1 if @page.between?(1, total_pages - 1) - previous_page = @page - 1 if @page.between?(2, total_pages) - first_page = @page == 1 - last_page = @page == total_pages - out_of_range = !@page.between?(1,total_pages) - - { - count: @count, - current_page: @page, - next_page: next_page, - previous_page: previous_page, - total_pages: total_pages, - per_page: @per_page, - first_page: first_page, - last_page: last_page, - out_of_range: out_of_range - } - end - - def clean_results - @results.map!{ |r| r.except("_query_full_count") } if @page && @per_page - end - - def load_associations - @results = QueryHelper::Associations.load_associations( - payload: @results, - associations: @associations, - as_json_options: @as_json_options - ) - end - - def determine_count - # Determine total result count (unpaginated) - @count = @page && @per_page && results.length > 0 ? results.first["_query_full_count"] : results.length - end - end -end diff --git a/lib/query_helper/query_helper_concern.rb b/lib/query_helper/query_helper_concern.rb index 04c4186..9efa915 100644 --- a/lib/query_helper/query_helper_concern.rb +++ b/lib/query_helper/query_helper_concern.rb @@ -1,10 +1,18 @@ require 'active_support/concern' +require "query_helper/sql_filter" module QueryHelper module QueryHelperConcern extend ActiveSupport::Concern included do + def sql_filter + SqlFilter.new(filter_values: filters, column_maps: @column_maps) + end + + def sql_sort + + end def query_helper_params helpers = {} helpers[:filters] = params[:filter] if params[:filter] diff --git a/lib/query_helper/query_filter.rb b/lib/query_helper/sql_filter.rb similarity index 98% rename from lib/query_helper/query_filter.rb rename to lib/query_helper/sql_filter.rb index b4dbeab..8aecd37 100644 --- a/lib/query_helper/query_filter.rb +++ b/lib/query_helper/sql_filter.rb @@ -1,5 +1,5 @@ module QueryHelper - class QueryFilter + class SqlFilter attr_accessor :filters, :where_filter_strings, :having_filter_strings, :bind_variables diff --git a/lib/query_helper/sql_manipulator.rb b/lib/query_helper/sql_manipulator.rb index 3542501..9345639 100644 --- a/lib/query_helper/sql_manipulator.rb +++ b/lib/query_helper/sql_manipulator.rb @@ -18,7 +18,6 @@ def initialize( @having_clauses = having_clauses @order_by_clauses = order_by_clauses @include_limit_clause = include_limit_clause - build() end def build diff --git a/lib/query_helper/sql_parser.rb b/lib/query_helper/sql_parser.rb index 2bd3ee3..105d1f8 100644 --- a/lib/query_helper/sql_parser.rb +++ b/lib/query_helper/sql_parser.rb @@ -1,3 +1,5 @@ +require "query_helper/column_map" + module QueryHelper class SqlParser @@ -148,6 +150,34 @@ def limit_clause @sql[limit_index()..insert_limit_index()] if limit_included? end + def find_aliases + # Determine alias expression combos. White out sql used in case there + # are any custom strings or subqueries in the select clause + white_out_selects = @white_out_sql[select_index(:end)..from_index()] + selects = @sql[select_index(:end)..from_index()] + comma_split_points = white_out_selects.each_char.with_index.map{|char, i| i if char == ','}.compact + comma_split_points.unshift(-1) # We need the first select clause to start out with a 'split' + column_maps = white_out_selects.split(",").each_with_index.map do |x,i| + sql_alias = x.squish.split(" as ")[1] || x.squish.split(" AS ")[1] || x.squish.split(".")[1] # look for custom defined aliases or table.column notation + sql_alias = nil unless /^[a-zA-Z_]+$/.match?(sql_alias) # only allow aliases with letters and underscores + sql_expression = if x.split(" as ")[1] + expression_length = x.split(" as ")[0].length + selects[comma_split_points[i] + 1, expression_length] + elsif x.squish.split(" AS ")[1] + expression_length = x.split(" AS ")[0].length + selects[comma_split_points[i] + 1, expression_length] + elsif x.squish.split(".")[1] + selects[comma_split_points[i] + 1, x.length] + end + ColumnMap.new( + alias_name: sql_alias, + sql_expression: sql_expression.squish, + aggregate: /(array_agg|avg|bit_and|bit_or|bool_and|bool_or|count|every|json_agg|jsonb_agg|json_object_agg|jsonb_object_agg|max|min|string_agg|sum|xmlagg)\((.*)\)/.match?(sql_expression) + ) if sql_alias + end + column_maps.compact + end + private def find_index(regex, position=:start) diff --git a/lib/query_helper/sql_runner.rb b/lib/query_helper/sql_runner.rb new file mode 100644 index 0000000..d115ade --- /dev/null +++ b/lib/query_helper/sql_runner.rb @@ -0,0 +1,116 @@ +require "query_helper/sql_manipulator" +require "query_helper/associations" + +module QueryHelper + class SqlRunner + + attr_accessor :results + + def initialize(**sql_params) + update(**sql_params) + end + + def update( + model:, # the model to run the query against + sql:, # a sql string + bind_variables: {}, # a list of bind variables to be embedded into the query + sql_filter: nil, # a SqlFilter object + sql_sort: nil, # a SqlSort object + page: nil, # define the page you want returned + per_page: nil, # define how many results you want per page + single_record: nil, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned + associations: nil, # a list of activerecord associations you'd like included in the payload + as_json_options: nil, # a list of as_json options you'd like run before returning the payload + run: nil # whether or not you'd like to run the query on initilization or update + ) + @model = model if model + @sql = sql if sql + @bind_variables = bind_variables if bind_variables + @sql_filter = sql_filter if sql_filter + @sql_sort = sql_sort if sql_sort + @page = page.to_i if page + @per_page = per_page.to_i if per_page + @single_record = single_record if single_record + @associations = associations if associations + @as_json_options = as_json_options if as_json_options + + # Determine limit and offset + limit = @per_page + offset = (@page - 1) * @per_page + + # Merge the filter variables and limit/offset variables into bind_variables + @bind_variables.merge!(@sql_filter.bind_variables).merge!{limit: limit, offset: offset} + + execute_query() if run + end + + def execute_query + # Execute Sql Query + manipulator = SqlManipulator.new( + sql: @sql, + where_clauses: @sql_filter.where_filter_strings, + having_clauses: @sql_filter.having_filter_strings, + order_by_clauses: @sql_sort.sort_strings, + include_limit_clause: @page && @per_page ? true : false + ) + + @results = @model.find_by_sql([manipulator.build(), @bind_variables]) # Execute Sql Query + @results = @results.first if @single_record # Return a single result if requested + + determine_count() + load_associations() + clean_results() + end + + def payload + if @page && @per_page + { + pagination: pagination_results(), + data: @results + } + else + { data: @results } + end + end + + private + + def pagination_results + total_pages = (@count/(@per_page.nonzero? || 1).to_f).ceil + next_page = @page + 1 if @page.between?(1, total_pages - 1) + previous_page = @page - 1 if @page.between?(2, total_pages) + first_page = @page == 1 + last_page = @page == total_pages + out_of_range = !@page.between?(1,total_pages) + + { + count: @count, + current_page: @page, + next_page: next_page, + previous_page: previous_page, + total_pages: total_pages, + per_page: @per_page, + first_page: first_page, + last_page: last_page, + out_of_range: out_of_range + } + end + + def clean_results + @results.map!{ |r| r.except("_query_full_count") } if @page && @per_page + end + + def load_associations + @results = Associations.load_associations( + payload: @results, + associations: @associations, + as_json_options: @as_json_options + ) + end + + def determine_count + # Determine total result count (unpaginated) + @count = @page && @per_page && results.length > 0 ? results.first["_query_full_count"] : results.length + end + end +end diff --git a/lib/query_helper/sort.rb b/lib/query_helper/sql_sort.rb similarity index 98% rename from lib/query_helper/sort.rb rename to lib/query_helper/sql_sort.rb index ae0fa85..a33929e 100644 --- a/lib/query_helper/sort.rb +++ b/lib/query_helper/sql_sort.rb @@ -1,5 +1,5 @@ module QueryHelper - class Sort + class SqlSort attr_accessor :sort_strings From a2c5dcd1e8d3a0b6f823c985655e9ae16e94726c Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Fri, 12 Jul 2019 12:48:01 -0600 Subject: [PATCH 36/48] Use custom errors --- lib/query_helper/filter.rb | 4 ++-- lib/query_helper/sql_parser.rb | 6 +++--- lib/query_helper/sql_sort.rb | 2 +- spec/query_helper/filter_spec.rb | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/query_helper/filter.rb b/lib/query_helper/filter.rb index 35c0885..5b5a7aa 100644 --- a/lib/query_helper/filter.rb +++ b/lib/query_helper/filter.rb @@ -62,7 +62,7 @@ def translate_operator_code "is not null" end else - raise ArgumentError.new("Invalid operator code: '#{operator_code}'") + raise InvalidQueryError.new("Invalid operator code: '#{operator_code}'") end end @@ -104,7 +104,7 @@ def validate_criterion end def invalid_criterion_error - raise ArgumentError.new("'#{criterion}' is not a valid criterion for the '#{operator}' operator") + raise InvalidQueryError.new("'#{criterion}' is not a valid criterion for the '#{operator}' operator") end end end diff --git a/lib/query_helper/sql_parser.rb b/lib/query_helper/sql_parser.rb index 105d1f8..0239530 100644 --- a/lib/query_helper/sql_parser.rb +++ b/lib/query_helper/sql_parser.rb @@ -108,17 +108,17 @@ def insert_where_index end def insert_having_index - # raise ArgumentError.new("Cannot calculate insert_having_index because the query has no group by clause") unless group_by_included? + # raise InvalidQueryError.new("Cannot calculate insert_having_index because the query has no group by clause") unless group_by_included? order_by_index() || limit_index() || @sql.length end def insert_order_by_index - # ArgumentError.new("This query already includes an order by clause") if order_by_included? + # raise InvalidQueryError.new("This query already includes an order by clause") if order_by_included? limit_index() || @sql.length end def insert_limit_index - # ArgumentError.new("This query already includes a limit clause") if limit_included? + # raise InvalidQueryError.new("This query already includes a limit clause") if limit_included? @sql.length end diff --git a/lib/query_helper/sql_sort.rb b/lib/query_helper/sql_sort.rb index a33929e..750d257 100644 --- a/lib/query_helper/sql_sort.rb +++ b/lib/query_helper/sql_sort.rb @@ -20,7 +20,7 @@ def parse_sort_string begin sql_expression = @column_maps.find{ |m| m.alias_name == sort_alias }.sql_expression rescue NoMethodError => e - raise ArgumentError.new("Sorting not allowed on column '#{sort_alias}'") + raise InvalidQueryError.new("Sorting not allowed on column '#{sort_alias}'") end if direction == "desc" diff --git a/spec/query_helper/filter_spec.rb b/spec/query_helper/filter_spec.rb index 5321d29..ba67b4e 100644 --- a/spec/query_helper/filter_spec.rb +++ b/spec/query_helper/filter_spec.rb @@ -46,7 +46,7 @@ criterion: Faker::Number.between(0, 100), comparate: "children.age" ) - }.to raise_error(ArgumentError) + }.to raise_error(InvalidQueryError) end end end @@ -60,7 +60,7 @@ RSpec.shared_examples "invalidates criterion" do it "validates criterion" do - expect{filter.send(:validate_criterion)}.to raise_error(ArgumentError) + expect{filter.send(:validate_criterion)}.to raise_error(InvalidQueryError) end end From 61d76a1d9f4409e21c25833574711f3dd68f8395 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Mon, 15 Jul 2019 09:52:36 -0600 Subject: [PATCH 37/48] WIP --- lib/query_helper.rb | 150 +++++++++++++++++- lib/query_helper/active_record_query.rb | 34 ---- lib/query_helper/column_map.rb | 34 +--- lib/query_helper/filter.rb | 6 +- lib/query_helper/query_helper_concern.rb | 21 ++- lib/query_helper/query_string.rb | 140 ---------------- lib/query_helper/sql.rb | 112 ------------- lib/query_helper/sql_filter.rb | 45 +++--- lib/query_helper/sql_runner.rb | 116 -------------- lib/query_helper/sql_sort.rb | 9 +- .../controllers/application_controller.rb | 4 + .../controllers/parents_controller.rb | 9 ++ spec/mock_rails_app/mock_rails_app.rb | 42 +++++ .../models/application_record.rb | 5 + spec/mock_rails_app/models/child.rb | 3 + spec/mock_rails_app/models/parent.rb | 7 + spec/query_helper_spec.rb | 7 +- 17 files changed, 265 insertions(+), 479 deletions(-) delete mode 100644 lib/query_helper/active_record_query.rb delete mode 100644 lib/query_helper/query_string.rb delete mode 100644 lib/query_helper/sql.rb delete mode 100644 lib/query_helper/sql_runner.rb create mode 100644 spec/mock_rails_app/controllers/application_controller.rb create mode 100644 spec/mock_rails_app/controllers/parents_controller.rb create mode 100644 spec/mock_rails_app/mock_rails_app.rb create mode 100644 spec/mock_rails_app/models/application_record.rb create mode 100644 spec/mock_rails_app/models/child.rb create mode 100644 spec/mock_rails_app/models/parent.rb diff --git a/lib/query_helper.rb b/lib/query_helper.rb index 32dd78f..b0e5168 100644 --- a/lib/query_helper.rb +++ b/lib/query_helper.rb @@ -3,11 +3,6 @@ require "query_helper/version" require "query_helper/filter" require "query_helper/column_map" -require "query_helper/query_string" -require "query_helper/sql" -require "query_helper/active_record_query" -require "query_helper/query_filter" -require "query_helper/sort" require "query_helper/associations" require "query_helper/query_helper_concern" require "query_helper/sql_parser" @@ -15,7 +10,150 @@ module QueryHelper + class InvalidQueryError < StandardError; end + class << self - attr_accessor :active_record_adapter + attr_accessor :active_record_adapter, :model, :query, :bind_variables, :sql_filter, :sql_sort, :page, :per_page, :single_record, :associations, :as_json_options + + def initialize( + model:, # the model to run the query against + query:, # a sql string or an active record query + bind_variables: {}, # a list of bind variables to be embedded into the query + sql_filter: nil, # a SqlFilter object + sql_sort: nil, # a SqlSort object + page: nil, # define the page you want returned + per_page: nil, # define how many results you want per page + single_record: false, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned + associations: nil, # a list of activerecord associations you'd like included in the payload + as_json_options: nil, # a list of as_json options you'd like run before returning the payload + custom_mappings: nil # custom keyword => sql_expression mappings + ) + @model = model + @query = sql + @bind_variables = bind_variables + @sql_filter = sql_filter + @sql_sort = sql_sort + @page = page.to_i if page + @per_page = per_page.to_i if per_page + @single_record = single_record + @associations = associations + @as_json_options = as_json_options + @custom_mappings = custom_mappings + + # Determine limit and offset + limit = @per_page + offset = (@page - 1) * @per_page + + # Merge limit/offset variables into bind_variables + @bind_variables.merge!({limit: limit, offset: offset}) + end + + def execute_query + + # Correctly set the query and model based on query type + determine_query_type() + + # Create column maps to be used by the filter and sort objects + column_maps = create_column_maps() + @sql_filter.column_maps = column_maps + @sql_sort.column_maps = column_maps + + # create the filters from the column maps + @sql_filter.create_filters + + # merge the filter bind variables into the query bind variables + @bind_variables.merge!(@sql_filter.bind_variables) + + # Execute Sql Query + manipulator = SqlManipulator.new( + sql: @query, + where_clauses: @sql_filter.where_clauses, + having_clauses: @sql_filter.having_clauses, + order_by_clauses: @sql_sort.parse_sort_string, + include_limit_clause: @page && @per_page ? true : false + ) + + @results = @model.find_by_sql([manipulator.build(), @bind_variables]) # Execute Sql Query + @results = @results.first if @single_record # Return a single result if requested + + determine_count() + load_associations() + clean_results() + end + + def results + execute_query() + @results + end + + def paginated_results + execute_query() + { pagination: pagination_results(), + data: @results } + end + + + private + + def determine_query_type + # If a custom sql string is passed in, make sure a valid model is passed in as well + if @query.class == String + raise InvalidQueryError.new("a valid model must be included to run a custom SQL query") unless @model < ActiveRecord::Base + # If an active record query is passed in, find the model and sql from the query + elsif @query.class < ActiveRecord::Relation + @model = query.model + @query = query.to_sql + else + raise InvalidQueryError.new("unable to determine query type") + end + end + + def determine_count + # Determine total result count (unpaginated) + @count = @page && @per_page && results.length > 0 ? results.first["_query_full_count"] : results.length + end + + def load_associations + @results = Associations.load_associations( + payload: @results, + associations: @associations, + as_json_options: @as_json_options + ) + end + + def clean_results + @results.map!{ |r| r.except("_query_full_count") } if @page && @per_page + end + + def pagination_results + # Set pagination params if they aren't provided + @per_page = @count unless @per_page + @page = 1 unless @page + + total_pages = (@count/(@per_page.nonzero? || 1).to_f).ceil + next_page = @page + 1 if @page.between?(1, total_pages - 1) + previous_page = @page - 1 if @page.between?(2, total_pages) + first_page = @page == 1 + last_page = @page == total_pages + out_of_range = !@page.between?(1,total_pages) + + { count: @count, + current_page: @page, + next_page: next_page, + previous_page: previous_page, + total_pages: total_pages, + per_page: @per_page, + first_page: first_page, + last_page: last_page, + out_of_range: out_of_range } + end + + def create_column_maps + ColumnMap.create_column_mappings( + query: @query, + custom_mappings: @custom_mappings + ) + end + end end diff --git a/lib/query_helper/active_record_query.rb b/lib/query_helper/active_record_query.rb deleted file mode 100644 index 78ab1a3..0000000 --- a/lib/query_helper/active_record_query.rb +++ /dev/null @@ -1,34 +0,0 @@ -module QueryHelper - class ActiveRecordQuery < Sql - - def initialize( - active_record_call:, # the active_record_query to be executed - query_params: {}, # a list of bind variables to be embedded into the query - column_mappings: {}, # A hash that translates aliases to sql expressions - filters: {}, # a list of filters in the form of {"comparate_alias"=>{"operator_code"=>"value"}}. i.e. {"age"=>{"lt"=>100}, "children_count"=>{"gt"=>0}} - sorts: "", # a comma separated string with a list of sort values i.e "age:desc,name:asc:lowercase" - page: nil, # define the page you want returned - per_page: nil, # define how many results you want per page - single_record: false, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned - associations: [], # a list of activerecord associations you'd like included in the payload - as_json_options: {}, # a list of as_json options you'd like run before returning the payload - run: true # whether or not you'd like to run the query on initilization - ) - - super( - model: active_record_call.model, - query: active_record_call.to_sql, - query_params: query_params, - column_mappings: column_mappings, - filters: filters, - sorts: sorts, - page: page, - per_page: per_page, - single_record: single_record, - associations: associations, - as_json_options: as_json_options, - run: run - ) - end - end -end diff --git a/lib/query_helper/column_map.rb b/lib/query_helper/column_map.rb index 7c5a4c1..56e4032 100644 --- a/lib/query_helper/column_map.rb +++ b/lib/query_helper/column_map.rb @@ -4,10 +4,10 @@ module QueryHelper class ColumnMap def self.create_column_mappings(custom_mappings:, query:) - default = find_aliases_in_query(query) + parser = SqlParser.new(query) maps = create_from_hash(custom_mappings) - default.each do |m| + parser.find_aliases.each do |m| maps << m if maps.select{|x| x.alias_name == m.alias_name}.empty? end @@ -34,36 +34,6 @@ def self.create_from_hash(hash) map end - def self.find_aliases_in_query(query) - # Determine alias expression combos. White out sql used in case there - # are any custom strings or subqueries in the select clause - select_index = QueryHelper::QueryString.last_select_index(query) - from_index = QueryHelper::QueryString.last_from_index(query) - white_out_select = QueryHelper::QueryString.white_out_query(query)[select_index..from_index] - select_clause = query[select_index..from_index] - comma_split_points = white_out_select.each_char.with_index.map{|char, i| i if char == ','}.compact - comma_split_points.unshift(-1) # We need the first select clause to start out with a 'split' - column_maps = white_out_select.split(",").each_with_index.map do |x,i| - sql_alias = x.squish.split(" as ")[1] || x.squish.split(" AS ")[1] || x.squish.split(".")[1] # look for custom defined aliases or table.column notation - sql_alias = nil unless /^[a-zA-Z_]+$/.match?(sql_alias) # only allow aliases with letters and underscores - sql_expression = if x.split(" as ")[1] - expression_length = x.split(" as ")[0].length - select_clause[comma_split_points[i] + 1, expression_length] - elsif x.squish.split(" AS ")[1] - expression_length = x.split(" AS ")[0].length - select_clause[comma_split_points[i] + 1, expression_length] - elsif x.squish.split(".")[1] - select_clause[comma_split_points[i] + 1, x.length] - end - QueryHelper::ColumnMap.new( - alias_name: sql_alias, - sql_expression: sql_expression.squish, - aggregate: /(array_agg|avg|bit_and|bit_or|bool_and|bool_or|count|every|json_agg|jsonb_agg|json_object_agg|jsonb_object_agg|max|min|string_agg|sum|xmlagg)\((.*)\)/.match?(sql_expression) - ) if sql_alias - end - column_maps.compact - end - attr_accessor :alias_name, :sql_expression, :aggregate def initialize( diff --git a/lib/query_helper/filter.rb b/lib/query_helper/filter.rb index 5b5a7aa..19d3ec5 100644 --- a/lib/query_helper/filter.rb +++ b/lib/query_helper/filter.rb @@ -1,18 +1,16 @@ module QueryHelper class Filter - attr_accessor :operator, :criterion, :comparate, :operator_code, :aggregate, :bind_variable + attr_accessor :operator, :criterion, :comparate, :operator_code, :bind_variable def initialize( operator_code:, criterion:, - comparate:, - aggregate: false + comparate: ) @operator_code = operator_code @criterion = criterion # Converts to a string to be inserted into sql. @comparate = comparate - @aggregate = aggregate @bind_variable = ('a'..'z').to_a.shuffle[0,20].join.to_sym translate_operator_code() diff --git a/lib/query_helper/query_helper_concern.rb b/lib/query_helper/query_helper_concern.rb index 9efa915..44f5519 100644 --- a/lib/query_helper/query_helper_concern.rb +++ b/lib/query_helper/query_helper_concern.rb @@ -6,20 +6,29 @@ module QueryHelperConcern extend ActiveSupport::Concern included do - def sql_filter - SqlFilter.new(filter_values: filters, column_maps: @column_maps) + def query_helper + @query_helper = QueryHelper.new(**query_helper_params) end - def sql_sort + def create_query_helper_filter end + + def create_query_helper_sort + + end + + def create_query_helper_associations + + end + def query_helper_params helpers = {} - helpers[:filters] = params[:filter] if params[:filter] - helpers[:sorts] = params[:sort] if params[:sort] helpers[:page] = params[:page] if params[:page] helpers[:per_page] = params[:per_page] if params[:per_page] - helpers[:associations] = params[:include] if params[:include] + helpers[:filters] = create_query_helper_filter() if params[:filter] + helpers[:sorts] = create_query_helper_sort() if params[:sort] + helpers[:associations] = create_query_helper_associations() if params[:include] helpers end end diff --git a/lib/query_helper/query_string.rb b/lib/query_helper/query_string.rb deleted file mode 100644 index 3274c7a..0000000 --- a/lib/query_helper/query_string.rb +++ /dev/null @@ -1,140 +0,0 @@ -module QueryHelper - class QueryString - - attr_accessor :query_string, :where_filters, :having_filters, :sorts, :page, :per_page, :alias_map - - # I've opted to make several methods class methods - # in order to utilize them in other parts of the gem - - def self.remove_comments(query) - # Remove sql comments - query.gsub(/\/\*(.*?)\*\//, '').gsub(/--(.*)$/, '') - end - - def self.white_out_query(query) - # Replace everything between () and '' and "" to find indexes. - # This will allow us to ignore subqueries and common table expressions when determining injection points - white_out = query.dup - while white_out.scan(/\"[^""]*\"|\'[^'']*\'|\([^()]*\)/).length > 0 do - white_out.scan(/\"[^""]*\"|\'[^'']*\'|\([^()]*\)/).each { |s| white_out.gsub!(s,s.gsub(/./, '*')) } - end - white_out - end - - def self.last_select_index(query) - # space or new line at beginning of select - # return index at the end of the word - query.rindex(/( |^)[Ss][Ee][Ll][Ee][Cc][Tt] /) + query[/( |^)[Ss][Ee][Ll][Ee][Cc][Tt] /].size - end - - def self.last_from_index(query) - query.index(/ [Ff][Rr][Oo][Mm] /, last_select_index(query)) - end - - def self.last_where_index(query) - query.index(/ [Ww][Hh][Ee][Rr][Ee] /, last_select_index(query)) - end - - def self.last_group_by_index(query) - query.index(/ [Gg][Rr][Oo][Uu][Pp] [Bb][Yy] /, last_select_index(query)) - end - - def self.last_having_index(query) - query.index(/ [Hh][Aa][Vv][Ii][Nn][Gg] /, last_select_index(query)) - end - - def self.last_order_by_index(query) - query.index(/ [Oo][Rr][Dd][Ee][Rr] [Bb][Yy ]/, last_select_index(query)) - end - - def initialize( - sql:, - where_filters: [], - having_filters: [], - sorts: [], - page: nil, - per_page: nil - ) - @sql = self.class.remove_comments(sql).squish - @where_filters = where_filters - @having_filters = having_filters - @sorts = sorts - @page = page.to_i if page # Turn into an integer to avoid any potential sql injection - @per_page = per_page.to_i if per_page # Turn into an integer to avoid any potential sql injection - calculate_indexes() - true - end - - def build - modified_sql = @sql.dup - modified_sql = modified_sql.slice(0, @last_order_by_index) if @order_by_included # Remove previous sorting if it exists - modified_sql.insert(modified_sql.length, pagination_insert) if @page && @per_page - modified_sql.insert(@insert_order_by_index, sort_insert) if @sorts && @sorts.length > 0 - modified_sql.insert(@insert_having_index, having_insert) if @having_filters && @having_filters.length > 0 - modified_sql.insert(@insert_where_index, where_insert) if @where_filters && @where_filters.length > 0 - modified_sql.insert(@insert_select_index, total_count_select_insert) if @page && @per_page - modified_sql.squish - end - - def update( - where_filters: nil, - having_filters: nil, - sorts: nil, - page: nil, - per_page: nil - ) - @where_filters = where_filters if where_filters - @having_filters = having_filters if having_filters - @sorts = sorts if sorts - @page = page if page - @per_page = per_page if per_page - end - - private - - def calculate_indexes - white_out_sql = self.class.white_out_query(@sql) - - @where_included = !self.class.last_where_index(white_out_sql).nil? - @group_by_included = !self.class.last_group_by_index(white_out_sql).nil? - @having_included = !self.class.last_having_index(white_out_sql).nil? - @order_by_included = !self.class.last_order_by_index(white_out_sql).nil? - - @insert_where_index = self.class.last_group_by_index(white_out_sql) || self.class.last_order_by_index(white_out_sql) || white_out_sql.length - @insert_having_index = self.class.last_order_by_index(white_out_sql) || white_out_sql.length - @insert_order_by_index = white_out_sql.length - @insert_join_index = self.class.last_where_index(white_out_sql) || self.class.last_group_by_index(white_out_sql) || self.class.last_order_by_index(white_out_sql) || white_out_sql.length - @insert_select_index = self.class.last_from_index(white_out_sql) - end - - def where_insert - begin_string = @where_included ? "and" : "where" - filter_string = @where_filters.join(" and ") - " #{begin_string} #{filter_string} " - end - - def having_insert - raise ArgumentError.new("Cannot include a having filter unless there is a group by clause in the query") unless @group_by_included - begin_string = @having_included ? "and" : "having" - filter_string = @having_filters.join(" and ") - " #{begin_string} #{filter_string} " - end - - def sort_insert - " order by #{sorts.join(", ")} " - end - - def pagination_insert - raise ArgumentError.new("page and per_page must be integers") unless @page.class == Integer && @per_page.class == Integer - limit = @per_page - offset = (@page - 1) * @per_page - " limit #{limit} offset #{offset} " - end - - def total_count_select_insert - " ,count(*) over () as _query_full_count " - end - - - end -end diff --git a/lib/query_helper/sql.rb b/lib/query_helper/sql.rb deleted file mode 100644 index f6f224a..0000000 --- a/lib/query_helper/sql.rb +++ /dev/null @@ -1,112 +0,0 @@ -module QueryHelper - class Sql - - attr_accessor :model, :query_string, :query_params, :query_filter, :results - - def initialize( - model:, # the model to run the query against - query:, # the custom sql to be executed - query_params: {}, # a list of bind variables to be embedded into the query - column_mappings: {}, # A hash that translates aliases to sql expressions - filters: {}, # a list of filters in the form of {"comparate_alias"=>{"operator_code"=>"value"}}. i.e. {"age"=>{"lt"=>100}, "children_count"=>{"gt"=>0}} - sorts: "", # a comma separated string with a list of sort values i.e "age:desc,name:asc:lowercase" - page: nil, # define the page you want returned - per_page: nil, # define how many results you want per page - single_record: false, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned - associations: [], # a list of activerecord associations you'd like included in the payload - as_json_options: {}, # a list of as_json options you'd like run before returning the payload - run: true # whether or not you'd like to run the query on initilization - ) - @model = model - @query_params = query_params - @page = page.to_i if page - @per_page = per_page.to_i if per_page - @single_record = single_record - @as_json_options = as_json_options - - # Create our column maps. - @column_maps = QueryHelper::ColumnMap.create_column_mappings(custom_mappings: column_mappings, query: query) - - # Create the filter and sort objects - @query_filter = QueryHelper::QueryFilter.new(filter_values: filters, column_maps: @column_maps) - @sorts = QueryHelper::Sort.new(sort_string: sorts, column_maps: @column_maps) - @associations = QueryHelper::Associations.process_association_params(associations) - - # create the query string object with the filters and sorts - @query_string = QueryHelper::QueryString.new( - sql: query, - page: @page, - per_page: @per_page, - where_filters: @query_filter.where_filter_strings, - having_filters: @query_filter.having_filter_strings, - sorts: @sorts.sort_strings, - ) - - # Merge the filter bind variables into the query_params - @query_params.merge!(@query_filter.bind_variables) - - execute_query() if run - end - - def execute_query - # Execute Sql Query - @results = @model.find_by_sql([@query_string.build(), @query_params]) # Execute Sql Query - @results = @results.first if @single_record # Return a single result if requested - - determine_count() - load_associations() - clean_results() - end - - def payload - if @page && @per_page - { - pagination: pagination_results(), - data: @results - } - else - { data: @results } - end - end - - private - - def pagination_results - total_pages = (@count/(@per_page.nonzero? || 1).to_f).ceil - next_page = @page + 1 if @page.between?(1, total_pages - 1) - previous_page = @page - 1 if @page.between?(2, total_pages) - first_page = @page == 1 - last_page = @page == total_pages - out_of_range = !@page.between?(1,total_pages) - - { - count: @count, - current_page: @page, - next_page: next_page, - previous_page: previous_page, - total_pages: total_pages, - per_page: @per_page, - first_page: first_page, - last_page: last_page, - out_of_range: out_of_range - } - end - - def clean_results - @results.map!{ |r| r.except("_query_full_count") } if @page && @per_page - end - - def load_associations - @results = QueryHelper::Associations.load_associations( - payload: @results, - associations: @associations, - as_json_options: @as_json_options - ) - end - - def determine_count - # Determine total result count (unpaginated) - @count = @page && @per_page && results.length > 0 ? results.first["_query_full_count"] : results.length - end - end -end diff --git a/lib/query_helper/sql_filter.rb b/lib/query_helper/sql_filter.rb index 8aecd37..3303a97 100644 --- a/lib/query_helper/sql_filter.rb +++ b/lib/query_helper/sql_filter.rb @@ -1,43 +1,40 @@ module QueryHelper class SqlFilter - attr_accessor :filters, :where_filter_strings, :having_filter_strings, :bind_variables + attr_accessor :filter_values, :column_maps def initialize(filter_values:, column_maps:) @column_maps = column_maps @filter_values = filter_values - @filters = create_filters() - @where_filter_strings = filters.select{ |f| f.aggregate == false }.map(&:sql_string) - @having_filter_strings = filters.select{ |f| f.aggregate == true }.map(&:sql_string) - @bind_variables = Hash[filters.collect { |f| [f.bind_variable, f.criterion] }] end def create_filters - filters = [] - @filter_values.each do |comparate, criteria| - # Default values - aggregate = false + @filters = [] + @filter_values.each do |comparate_alias, criteria| # Find the sql mapping if it exists - map = @column_maps.find{ |m| m.alias_name == comparate } # Find the sql mapping if it exists - if map - comparate = map.sql_expression - aggregate = map.aggregate - end - - # Set the criteria - operator_code = criteria.keys.first - criterion = criteria.values.first + map = @column_maps.find { |m| m.alias_name == comparate_alias } + raise InvalidQueryError("cannot filter by #{comparate_alias}") unless map # create the filter - filters << QueryHelper::Filter.new( - operator_code: operator_code, - criterion: criterion, - comparate: comparate, - aggregate: aggregate, + @filters << QueryHelper::Filter.new( + operator_code: criteria.keys.first, + criterion: criteria.values.first, + comparate: map.sql_expression ) end - filters + end + + def where_clauses + @filters.select{ |f| f.aggregate == false }.map(&:sql_string) + end + + def having_clauses + @filters.select{ |f| f.aggregate == true }.map(&:sql_string) + end + + def bind_variables + Hash[@filters.collect { |f| [f.bind_variable, f.criterion] }] end end end diff --git a/lib/query_helper/sql_runner.rb b/lib/query_helper/sql_runner.rb deleted file mode 100644 index d115ade..0000000 --- a/lib/query_helper/sql_runner.rb +++ /dev/null @@ -1,116 +0,0 @@ -require "query_helper/sql_manipulator" -require "query_helper/associations" - -module QueryHelper - class SqlRunner - - attr_accessor :results - - def initialize(**sql_params) - update(**sql_params) - end - - def update( - model:, # the model to run the query against - sql:, # a sql string - bind_variables: {}, # a list of bind variables to be embedded into the query - sql_filter: nil, # a SqlFilter object - sql_sort: nil, # a SqlSort object - page: nil, # define the page you want returned - per_page: nil, # define how many results you want per page - single_record: nil, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned - associations: nil, # a list of activerecord associations you'd like included in the payload - as_json_options: nil, # a list of as_json options you'd like run before returning the payload - run: nil # whether or not you'd like to run the query on initilization or update - ) - @model = model if model - @sql = sql if sql - @bind_variables = bind_variables if bind_variables - @sql_filter = sql_filter if sql_filter - @sql_sort = sql_sort if sql_sort - @page = page.to_i if page - @per_page = per_page.to_i if per_page - @single_record = single_record if single_record - @associations = associations if associations - @as_json_options = as_json_options if as_json_options - - # Determine limit and offset - limit = @per_page - offset = (@page - 1) * @per_page - - # Merge the filter variables and limit/offset variables into bind_variables - @bind_variables.merge!(@sql_filter.bind_variables).merge!{limit: limit, offset: offset} - - execute_query() if run - end - - def execute_query - # Execute Sql Query - manipulator = SqlManipulator.new( - sql: @sql, - where_clauses: @sql_filter.where_filter_strings, - having_clauses: @sql_filter.having_filter_strings, - order_by_clauses: @sql_sort.sort_strings, - include_limit_clause: @page && @per_page ? true : false - ) - - @results = @model.find_by_sql([manipulator.build(), @bind_variables]) # Execute Sql Query - @results = @results.first if @single_record # Return a single result if requested - - determine_count() - load_associations() - clean_results() - end - - def payload - if @page && @per_page - { - pagination: pagination_results(), - data: @results - } - else - { data: @results } - end - end - - private - - def pagination_results - total_pages = (@count/(@per_page.nonzero? || 1).to_f).ceil - next_page = @page + 1 if @page.between?(1, total_pages - 1) - previous_page = @page - 1 if @page.between?(2, total_pages) - first_page = @page == 1 - last_page = @page == total_pages - out_of_range = !@page.between?(1,total_pages) - - { - count: @count, - current_page: @page, - next_page: next_page, - previous_page: previous_page, - total_pages: total_pages, - per_page: @per_page, - first_page: first_page, - last_page: last_page, - out_of_range: out_of_range - } - end - - def clean_results - @results.map!{ |r| r.except("_query_full_count") } if @page && @per_page - end - - def load_associations - @results = Associations.load_associations( - payload: @results, - associations: @associations, - as_json_options: @as_json_options - ) - end - - def determine_count - # Determine total result count (unpaginated) - @count = @page && @per_page && results.length > 0 ? results.first["_query_full_count"] : results.length - end - end -end diff --git a/lib/query_helper/sql_sort.rb b/lib/query_helper/sql_sort.rb index 750d257..432dcb0 100644 --- a/lib/query_helper/sql_sort.rb +++ b/lib/query_helper/sql_sort.rb @@ -1,16 +1,15 @@ module QueryHelper class SqlSort - attr_accessor :sort_strings + attr_accessor :column_maps def initialize(sort_string:, column_maps:) @sort_string = sort_string @column_maps = column_maps - @sort_strings = [] - parse_sort_string() end def parse_sort_string + sql_strings = [] sorts = @sort_string.split(",") sorts.each_with_index do |sort, index| sort_alias = sort.split(":")[0] @@ -39,8 +38,10 @@ def parse_sort_string sql_expression = "lower(#{sql_expression})" end - @sort_strings << "#{sql_expression} #{direction}" + sql_strings << "#{sql_expression} #{direction}" end + + return sql_strings end end end diff --git a/spec/mock_rails_app/controllers/application_controller.rb b/spec/mock_rails_app/controllers/application_controller.rb new file mode 100644 index 0000000..6418f98 --- /dev/null +++ b/spec/mock_rails_app/controllers/application_controller.rb @@ -0,0 +1,4 @@ +class ApplicationController < ActionController::API + include QueryHelperConcern + before_action :query_helper +end diff --git a/spec/mock_rails_app/controllers/parents_controller.rb b/spec/mock_rails_app/controllers/parents_controller.rb new file mode 100644 index 0000000..72b27cc --- /dev/null +++ b/spec/mock_rails_app/controllers/parents_controller.rb @@ -0,0 +1,9 @@ +class ParentsController < ApplicationController + def index + byebug + end + + def show + byebug + end +end diff --git a/spec/mock_rails_app/mock_rails_app.rb b/spec/mock_rails_app/mock_rails_app.rb new file mode 100644 index 0000000..7b824b6 --- /dev/null +++ b/spec/mock_rails_app/mock_rails_app.rb @@ -0,0 +1,42 @@ +require_relative('models/child') +require_relative('models/parent') + +module MockRailsApp + extend self + def setup + QueryHelper.active_record_adapter = "sqlite3" # Use sqlite3 in memory for test suites + create_tables() + populate_tables() + end + + def create_tables + # Set up a database that resides in RAM + ActiveRecord::Base.establish_connection( + adapter: QueryHelper.active_record_adapter, + database: ':memory:' + ) + + # Set up database tables and columns + ActiveRecord::Schema.define do + create_table :parents, force: true do |t| + t.string :name + t.integer :age + end + create_table :children, force: true do |t| + t.string :name + t.references :parent + t.integer :age + end + end + end + + def populate_tables + # Load data into databases + (0..99).each do + parent = Parent.create(name: Faker::Name.name, age: Faker::Number.between(25, 55)) + (0..Faker::Number.between(1, 5)).each do + Child.create(name: Faker::Name.name, parent: parent, age: Faker::Number.between(1, 25)) + end + end + end +end diff --git a/spec/mock_rails_app/models/application_record.rb b/spec/mock_rails_app/models/application_record.rb new file mode 100644 index 0000000..f2baebe --- /dev/null +++ b/spec/mock_rails_app/models/application_record.rb @@ -0,0 +1,5 @@ +module MockRailsApp + class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + end +end diff --git a/spec/mock_rails_app/models/child.rb b/spec/mock_rails_app/models/child.rb new file mode 100644 index 0000000..f629798 --- /dev/null +++ b/spec/mock_rails_app/models/child.rb @@ -0,0 +1,3 @@ +class Child < ApplicationRecord + belongs_to :parent +end diff --git a/spec/mock_rails_app/models/parent.rb b/spec/mock_rails_app/models/parent.rb new file mode 100644 index 0000000..fb646d7 --- /dev/null +++ b/spec/mock_rails_app/models/parent.rb @@ -0,0 +1,7 @@ +class Parent < ApplicationRecord + has_many :children + + def favorite_star_wars_character + Faker::Movies::StarWars.character + end +end diff --git a/spec/query_helper_spec.rb b/spec/query_helper_spec.rb index 40d3bc2..d995bed 100644 --- a/spec/query_helper_spec.rb +++ b/spec/query_helper_spec.rb @@ -1,3 +1,8 @@ +require "spec_helper" + RSpec.describe QueryHelper do - it "pending tests" + + it "text" do + byebug + end end From 18f5c7ad2945c3c8f0af6683f7ea65c491d1b4aa Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Mon, 15 Jul 2019 12:53:18 -0600 Subject: [PATCH 38/48] got a fake rails env running in rspec --- Gemfile.lock | 49 +++++++++++++++++++ query_helper.gemspec | 3 ++ spec/controller_test.rb | 22 +++++++++ spec/fixtures/application.rb | 22 +++++++++ spec/fixtures/controllers.rb | 15 ++++++ spec/fixtures/models.rb | 26 ++++++++++ spec/fixtures/routes.rb | 6 +++ .../controllers/application_controller.rb | 4 -- .../controllers/parents_controller.rb | 9 ---- spec/mock_rails_app/mock_rails_app.rb | 42 ---------------- .../models/application_record.rb | 5 -- spec/mock_rails_app/models/child.rb | 3 -- spec/mock_rails_app/models/parent.rb | 7 --- spec/query_helper_spec.rb | 17 +++++-- spec/spec_helper.rb | 42 ++++++++-------- 15 files changed, 176 insertions(+), 96 deletions(-) create mode 100644 spec/controller_test.rb create mode 100644 spec/fixtures/application.rb create mode 100644 spec/fixtures/controllers.rb create mode 100644 spec/fixtures/models.rb create mode 100644 spec/fixtures/routes.rb delete mode 100644 spec/mock_rails_app/controllers/application_controller.rb delete mode 100644 spec/mock_rails_app/controllers/parents_controller.rb delete mode 100644 spec/mock_rails_app/mock_rails_app.rb delete mode 100644 spec/mock_rails_app/models/application_record.rb delete mode 100644 spec/mock_rails_app/models/child.rb delete mode 100644 spec/mock_rails_app/models/parent.rb diff --git a/Gemfile.lock b/Gemfile.lock index 5b39d0c..2e5cc72 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,6 +8,19 @@ PATH GEM remote: https://rubygems.org/ specs: + actionpack (5.2.3) + actionview (= 5.2.3) + activesupport (= 5.2.3) + rack (~> 2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.2.3) + activesupport (= 5.2.3) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) activemodel (5.2.3) activesupport (= 5.2.3) activerecord (5.2.3) @@ -20,14 +33,38 @@ GEM minitest (~> 5.1) tzinfo (~> 1.1) arel (9.0.0) + builder (3.2.3) byebug (11.0.1) concurrent-ruby (1.1.4) + crass (1.0.4) diff-lcs (1.3) + erubi (1.8.0) faker (1.9.3) i18n (>= 0.7) i18n (1.6.0) concurrent-ruby (~> 1.0) + loofah (2.2.3) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + method_source (0.9.2) + mini_portile2 (2.4.0) minitest (5.11.3) + nokogiri (1.10.3) + mini_portile2 (~> 2.4.0) + rack (2.0.7) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) + railties (5.2.3) + actionpack (= 5.2.3) + activesupport (= 5.2.3) + method_source + rake (>= 0.8.7) + thor (>= 0.19.0, < 2.0) rake (10.5.0) rspec (3.8.0) rspec-core (~> 3.8.0) @@ -41,8 +78,17 @@ GEM rspec-mocks (3.8.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.8.0) + rspec-rails (3.8.2) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-support (~> 3.8.0) rspec-support (3.8.0) sqlite3 (1.3.13) + thor (0.20.3) thread_safe (0.3.6) tzinfo (1.2.5) thread_safe (~> 0.1) @@ -51,12 +97,15 @@ PLATFORMS ruby DEPENDENCIES + actionpack + activesupport bundler (~> 1.16) byebug faker (~> 1.9.3) query_helper! rake (~> 10.0) rspec (~> 3.0) + rspec-rails sqlite3 (~> 1.3.6) BUNDLED WITH diff --git a/query_helper.gemspec b/query_helper.gemspec index 233955e..897c06d 100644 --- a/query_helper.gemspec +++ b/query_helper.gemspec @@ -42,6 +42,9 @@ Gem::Specification.new do |spec| spec.add_development_dependency "sqlite3", "~> 1.3.6" spec.add_development_dependency "faker", "~> 1.9.3" spec.add_development_dependency "byebug" + spec.add_development_dependency 'rspec-rails' + spec.add_development_dependency 'actionpack' + spec.add_development_dependency 'activesupport' spec.add_dependency "activerecord", "~> 5.0" spec.add_dependency "activesupport", "~> 5.0" diff --git a/spec/controller_test.rb b/spec/controller_test.rb new file mode 100644 index 0000000..73a0701 --- /dev/null +++ b/spec/controller_test.rb @@ -0,0 +1,22 @@ +require 'fixtures/application' +require 'fixtures/controllers' +# require 'fixtures/routes' +require 'rspec/rails' + + RSpec.describe ParentsController, type: :controller do + describe '#index' do + it "text" do + get :index + byebug + # ... + end + + end +end + +# RSpec.describe 'Requests', type: :request do +# it "text" do +# get '/parents' +# byebug +# end +# end diff --git a/spec/fixtures/application.rb b/spec/fixtures/application.rb new file mode 100644 index 0000000..5f02659 --- /dev/null +++ b/spec/fixtures/application.rb @@ -0,0 +1,22 @@ +require 'active_support/all' +require 'action_controller' +require 'action_dispatch' +require 'active_record' + +module Rails + class App + def env_config; {} end + def routes + return @routes if defined?(@routes) + @routes = ActionDispatch::Routing::RouteSet.new + @routes.draw do + resources :parents + end + @routes + end + end + + def self.application + @app ||= App.new + end +end diff --git a/spec/fixtures/controllers.rb b/spec/fixtures/controllers.rb new file mode 100644 index 0000000..2ceff39 --- /dev/null +++ b/spec/fixtures/controllers.rb @@ -0,0 +1,15 @@ +class ApplicationController < ActionController::API + include Rails.application.routes.url_helpers + include QueryHelper::QueryHelperConcern + before_action :query_helper +end + +class ParentsController < ApplicationController + def index + byebug + end + + def show + byebug + end +end diff --git a/spec/fixtures/models.rb b/spec/fixtures/models.rb new file mode 100644 index 0000000..3b7a68a --- /dev/null +++ b/spec/fixtures/models.rb @@ -0,0 +1,26 @@ +# class Post +# extend ActiveModel::Naming +# include ActiveModel::Conversion +# attr_accessor :id +# +# def initialize(attributes={}) +# self.id = attributes +# end +# +# def persisted? +# true +# end +# end +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end +class Parent < ApplicationRecord + has_many :children + + def favorite_star_wars_character + Faker::Movies::StarWars.character + end +end +class Child < ApplicationRecord + belongs_to :parent +end diff --git a/spec/fixtures/routes.rb b/spec/fixtures/routes.rb new file mode 100644 index 0000000..d5ee432 --- /dev/null +++ b/spec/fixtures/routes.rb @@ -0,0 +1,6 @@ +Rails.application.routes.draw do + # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html + # Serve websocket cable requests in-process + + resources :parents, only: [:index, :show] +end diff --git a/spec/mock_rails_app/controllers/application_controller.rb b/spec/mock_rails_app/controllers/application_controller.rb deleted file mode 100644 index 6418f98..0000000 --- a/spec/mock_rails_app/controllers/application_controller.rb +++ /dev/null @@ -1,4 +0,0 @@ -class ApplicationController < ActionController::API - include QueryHelperConcern - before_action :query_helper -end diff --git a/spec/mock_rails_app/controllers/parents_controller.rb b/spec/mock_rails_app/controllers/parents_controller.rb deleted file mode 100644 index 72b27cc..0000000 --- a/spec/mock_rails_app/controllers/parents_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -class ParentsController < ApplicationController - def index - byebug - end - - def show - byebug - end -end diff --git a/spec/mock_rails_app/mock_rails_app.rb b/spec/mock_rails_app/mock_rails_app.rb deleted file mode 100644 index 7b824b6..0000000 --- a/spec/mock_rails_app/mock_rails_app.rb +++ /dev/null @@ -1,42 +0,0 @@ -require_relative('models/child') -require_relative('models/parent') - -module MockRailsApp - extend self - def setup - QueryHelper.active_record_adapter = "sqlite3" # Use sqlite3 in memory for test suites - create_tables() - populate_tables() - end - - def create_tables - # Set up a database that resides in RAM - ActiveRecord::Base.establish_connection( - adapter: QueryHelper.active_record_adapter, - database: ':memory:' - ) - - # Set up database tables and columns - ActiveRecord::Schema.define do - create_table :parents, force: true do |t| - t.string :name - t.integer :age - end - create_table :children, force: true do |t| - t.string :name - t.references :parent - t.integer :age - end - end - end - - def populate_tables - # Load data into databases - (0..99).each do - parent = Parent.create(name: Faker::Name.name, age: Faker::Number.between(25, 55)) - (0..Faker::Number.between(1, 5)).each do - Child.create(name: Faker::Name.name, parent: parent, age: Faker::Number.between(1, 25)) - end - end - end -end diff --git a/spec/mock_rails_app/models/application_record.rb b/spec/mock_rails_app/models/application_record.rb deleted file mode 100644 index f2baebe..0000000 --- a/spec/mock_rails_app/models/application_record.rb +++ /dev/null @@ -1,5 +0,0 @@ -module MockRailsApp - class ApplicationRecord < ActiveRecord::Base - self.abstract_class = true - end -end diff --git a/spec/mock_rails_app/models/child.rb b/spec/mock_rails_app/models/child.rb deleted file mode 100644 index f629798..0000000 --- a/spec/mock_rails_app/models/child.rb +++ /dev/null @@ -1,3 +0,0 @@ -class Child < ApplicationRecord - belongs_to :parent -end diff --git a/spec/mock_rails_app/models/parent.rb b/spec/mock_rails_app/models/parent.rb deleted file mode 100644 index fb646d7..0000000 --- a/spec/mock_rails_app/models/parent.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Parent < ApplicationRecord - has_many :children - - def favorite_star_wars_character - Faker::Movies::StarWars.character - end -end diff --git a/spec/query_helper_spec.rb b/spec/query_helper_spec.rb index d995bed..410229f 100644 --- a/spec/query_helper_spec.rb +++ b/spec/query_helper_spec.rb @@ -1,8 +1,15 @@ require "spec_helper" -RSpec.describe QueryHelper do +# RSpec.describe QueryHelper do +# +# it "text" do +# byebug +# end +# end - it "text" do - byebug - end -end +# RSpec.describe 'Requests', type: :request do +# it "text" do +# get '/parents' +# byebug +# end +# end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f22b710..01b7bfa 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,5 @@ -require "bundler/setup" -require "query_helper" +require 'bundler/setup' +require 'query_helper' require 'sqlite3' require 'active_record' require 'faker' @@ -58,25 +58,25 @@ end # Set up model classes - class ApplicationRecord < ActiveRecord::Base - self.abstract_class = true - end - class Parent < ApplicationRecord - has_many :children - - def favorite_star_wars_character - Faker::Movies::StarWars.character - end - end - class Child < ApplicationRecord - belongs_to :parent - end + # class ApplicationRecord < ActiveRecord::Base + # self.abstract_class = true + # end + # class Parent < ApplicationRecord + # has_many :children + # + # def favorite_star_wars_character + # Faker::Movies::StarWars.character + # end + # end + # class Child < ApplicationRecord + # belongs_to :parent + # end # Load data into databases - (0..99).each do - parent = Parent.create(name: Faker::Name.name, age: Faker::Number.between(25, 55)) - (0..Faker::Number.between(1, 5)).each do - Child.create(name: Faker::Name.name, parent: parent, age: Faker::Number.between(1, 25)) - end - end + # (0..99).each do + # parent = Parent.create(name: Faker::Name.name, age: Faker::Number.between(25, 55)) + # (0..Faker::Number.between(1, 5)).each do + # Child.create(name: Faker::Name.name, parent: parent, age: Faker::Number.between(1, 25)) + # end + # end end From e90d90a35c39a938a4b6366fb4ea82ac41291a12 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Mon, 15 Jul 2019 15:42:08 -0600 Subject: [PATCH 39/48] WIP --- lib/query_helper.rb | 41 +++++++++++++----------- lib/query_helper/associations.rb | 2 +- lib/query_helper/column_map.rb | 2 +- lib/query_helper/filter.rb | 2 +- lib/query_helper/query_helper_concern.rb | 6 +++- lib/query_helper/sql_filter.rb | 4 +-- lib/query_helper/sql_manipulator.rb | 13 ++++---- lib/query_helper/sql_parser.rb | 2 +- lib/query_helper/sql_sort.rb | 4 +-- lib/query_helper/version.rb | 2 +- spec/controller_test.rb | 6 ++-- spec/fixtures/controllers.rb | 8 +++-- spec/spec_helper.rb | 32 ++++++------------ 13 files changed, 61 insertions(+), 63 deletions(-) diff --git a/lib/query_helper.rb b/lib/query_helper.rb index b0e5168..09e0fce 100644 --- a/lib/query_helper.rb +++ b/lib/query_helper.rb @@ -7,29 +7,31 @@ require "query_helper/query_helper_concern" require "query_helper/sql_parser" require "query_helper/sql_manipulator" +require "query_helper/sql_filter" +require "query_helper/sql_sort" -module QueryHelper +class QueryHelper - class InvalidQueryError < StandardError; end + # class InvalidQueryError < StandardError; end - class << self + # class << self attr_accessor :active_record_adapter, :model, :query, :bind_variables, :sql_filter, :sql_sort, :page, :per_page, :single_record, :associations, :as_json_options def initialize( - model:, # the model to run the query against - query:, # a sql string or an active record query + model: nil, # the model to run the query against + query: nil, # a sql string or an active record query bind_variables: {}, # a list of bind variables to be embedded into the query - sql_filter: nil, # a SqlFilter object - sql_sort: nil, # a SqlSort object + sql_filter: SqlFilter.new(), # a SqlFilter object + sql_sort: SqlSort.new(), # a SqlSort object page: nil, # define the page you want returned per_page: nil, # define how many results you want per page single_record: false, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned associations: nil, # a list of activerecord associations you'd like included in the payload as_json_options: nil, # a list of as_json options you'd like run before returning the payload - custom_mappings: nil # custom keyword => sql_expression mappings + custom_mappings: {} # custom keyword => sql_expression mappings ) @model = model - @query = sql + @query = query @bind_variables = bind_variables @sql_filter = sql_filter @sql_sort = sql_sort @@ -40,15 +42,18 @@ def initialize( @as_json_options = as_json_options @custom_mappings = custom_mappings - # Determine limit and offset - limit = @per_page - offset = (@page - 1) * @per_page + if @page && @per_page + # Determine limit and offset + limit = @per_page + offset = (@page - 1) * @per_page - # Merge limit/offset variables into bind_variables - @bind_variables.merge!({limit: limit, offset: offset}) + # Merge limit/offset variables into bind_variables + @bind_variables.merge!({limit: limit, offset: offset}) + end end def execute_query + puts caller[0][/`.*'/][1..-2] # Correctly set the query and model based on query type determine_query_type() @@ -101,8 +106,8 @@ def determine_query_type raise InvalidQueryError.new("a valid model must be included to run a custom SQL query") unless @model < ActiveRecord::Base # If an active record query is passed in, find the model and sql from the query elsif @query.class < ActiveRecord::Relation - @model = query.model - @query = query.to_sql + @model = @query.model + @query = @query.to_sql else raise InvalidQueryError.new("unable to determine query type") end @@ -110,7 +115,7 @@ def determine_query_type def determine_count # Determine total result count (unpaginated) - @count = @page && @per_page && results.length > 0 ? results.first["_query_full_count"] : results.length + @count = @page && @per_page && @results.length > 0 ? @results.first["_query_full_count"] : @results.length end def load_associations @@ -155,5 +160,5 @@ def create_column_maps ) end - end + # end end diff --git a/lib/query_helper/associations.rb b/lib/query_helper/associations.rb index 8226a31..a14a2c3 100644 --- a/lib/query_helper/associations.rb +++ b/lib/query_helper/associations.rb @@ -1,4 +1,4 @@ -module QueryHelper +class QueryHelper class Associations def self.process_association_params(associations) associations ||= [] diff --git a/lib/query_helper/column_map.rb b/lib/query_helper/column_map.rb index 56e4032..2eb05ef 100644 --- a/lib/query_helper/column_map.rb +++ b/lib/query_helper/column_map.rb @@ -1,6 +1,6 @@ require "query_helper/sql_parser" -module QueryHelper +class QueryHelper class ColumnMap def self.create_column_mappings(custom_mappings:, query:) diff --git a/lib/query_helper/filter.rb b/lib/query_helper/filter.rb index 19d3ec5..04e6cb5 100644 --- a/lib/query_helper/filter.rb +++ b/lib/query_helper/filter.rb @@ -1,4 +1,4 @@ -module QueryHelper +class QueryHelper class Filter attr_accessor :operator, :criterion, :comparate, :operator_code, :bind_variable diff --git a/lib/query_helper/query_helper_concern.rb b/lib/query_helper/query_helper_concern.rb index 44f5519..de81e6d 100644 --- a/lib/query_helper/query_helper_concern.rb +++ b/lib/query_helper/query_helper_concern.rb @@ -1,12 +1,16 @@ require 'active_support/concern' require "query_helper/sql_filter" -module QueryHelper +class QueryHelper module QueryHelperConcern extend ActiveSupport::Concern included do def query_helper + @query_helper + end + + def create_query_helper @query_helper = QueryHelper.new(**query_helper_params) end diff --git a/lib/query_helper/sql_filter.rb b/lib/query_helper/sql_filter.rb index 3303a97..00a3d6d 100644 --- a/lib/query_helper/sql_filter.rb +++ b/lib/query_helper/sql_filter.rb @@ -1,9 +1,9 @@ -module QueryHelper +class QueryHelper class SqlFilter attr_accessor :filter_values, :column_maps - def initialize(filter_values:, column_maps:) + def initialize(filter_values: [], column_maps: nil) @column_maps = column_maps @filter_values = filter_values end diff --git a/lib/query_helper/sql_manipulator.rb b/lib/query_helper/sql_manipulator.rb index 9345639..04130fe 100644 --- a/lib/query_helper/sql_manipulator.rb +++ b/lib/query_helper/sql_manipulator.rb @@ -1,6 +1,6 @@ require "query_helper/sql_parser" -module QueryHelper +class QueryHelper class SqlManipulator attr_accessor :sql @@ -26,6 +26,7 @@ def build insert_having_clauses() insert_where_clauses() insert_total_count_select_clause() + @sql end private @@ -38,22 +39,22 @@ def insert_total_count_select_clause end def insert_where_clauses - return unless @where_clauses - begin_string = @parser.where_included ? "and" : "where" + return unless @where_clauses.length > 0 + begin_string = @parser.where_included? ? "and" : "where" filter_string = @where_clauses.join(" and ") " #{begin_string} #{filter_string} " @sql.insert(@parser.insert_where_index, " #{begin_string} #{filter_string} ") end def insert_having_clauses - return unless @having_clauses - begin_string = @parser.having_included ? "and" : "having" + return unless @having_clauses.length > 0 + begin_string = @parser.having_included? ? "and" : "having" filter_string = @having_clauses.join(" and ") @sql.insert(@parser.insert_having_index, " #{begin_string} #{filter_string} ") end def insert_order_by_clause - return unless @order_by_clauses + return unless @order_by_clauses.length > 0 @sql.slice!(@parser.order_by_clause) if @parser.order_by_included? # remove existing order by clause @sql.insert(@parser.insert_having_index, " order by #{@order_by_clauses.join(", ")} ") end diff --git a/lib/query_helper/sql_parser.rb b/lib/query_helper/sql_parser.rb index 0239530..4211297 100644 --- a/lib/query_helper/sql_parser.rb +++ b/lib/query_helper/sql_parser.rb @@ -1,6 +1,6 @@ require "query_helper/column_map" -module QueryHelper +class QueryHelper class SqlParser attr_accessor :sql diff --git a/lib/query_helper/sql_sort.rb b/lib/query_helper/sql_sort.rb index 432dcb0..f820894 100644 --- a/lib/query_helper/sql_sort.rb +++ b/lib/query_helper/sql_sort.rb @@ -1,9 +1,9 @@ -module QueryHelper +class QueryHelper class SqlSort attr_accessor :column_maps - def initialize(sort_string:, column_maps:) + def initialize(sort_string: "", column_maps: []) @sort_string = sort_string @column_maps = column_maps end diff --git a/lib/query_helper/version.rb b/lib/query_helper/version.rb index b9e3970..fb29c53 100644 --- a/lib/query_helper/version.rb +++ b/lib/query_helper/version.rb @@ -1,3 +1,3 @@ -module QueryHelper +class QueryHelper VERSION = "0.2.10" end diff --git a/spec/controller_test.rb b/spec/controller_test.rb index 73a0701..b60a6f0 100644 --- a/spec/controller_test.rb +++ b/spec/controller_test.rb @@ -1,16 +1,14 @@ require 'fixtures/application' require 'fixtures/controllers' -# require 'fixtures/routes' +require 'fixtures/models' require 'rspec/rails' RSpec.describe ParentsController, type: :controller do describe '#index' do it "text" do - get :index + get :index, params: {hello: "world"} byebug - # ... end - end end diff --git a/spec/fixtures/controllers.rb b/spec/fixtures/controllers.rb index 2ceff39..77fa150 100644 --- a/spec/fixtures/controllers.rb +++ b/spec/fixtures/controllers.rb @@ -1,15 +1,19 @@ +require 'fixtures/application' +require 'fixtures/models' + class ApplicationController < ActionController::API include Rails.application.routes.url_helpers include QueryHelper::QueryHelperConcern - before_action :query_helper + before_action :create_query_helper end class ParentsController < ApplicationController def index + @query_helper.query = Parent.all byebug + render json: @query_helper.paginated_results() end def show - byebug end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 01b7bfa..256328b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,6 +4,7 @@ require 'active_record' require 'faker' require 'byebug' +require 'fixtures/models' RSpec.configure do |config| # Enable flags like --only-failures and --next-failure @@ -36,11 +37,11 @@ } end - QueryHelper.active_record_adapter = "sqlite3" + # QueryHelper.active_record_adapter = "sqlite3" # Set up a database that resides in RAM ActiveRecord::Base.establish_connection( - adapter: QueryHelper.active_record_adapter, + adapter: "sqlite3", database: ':memory:' ) @@ -57,26 +58,11 @@ end end - # Set up model classes - # class ApplicationRecord < ActiveRecord::Base - # self.abstract_class = true - # end - # class Parent < ApplicationRecord - # has_many :children - # - # def favorite_star_wars_character - # Faker::Movies::StarWars.character - # end - # end - # class Child < ApplicationRecord - # belongs_to :parent - # end - # Load data into databases - # (0..99).each do - # parent = Parent.create(name: Faker::Name.name, age: Faker::Number.between(25, 55)) - # (0..Faker::Number.between(1, 5)).each do - # Child.create(name: Faker::Name.name, parent: parent, age: Faker::Number.between(1, 25)) - # end - # end + (0..99).each do + parent = Parent.create(name: Faker::Name.name, age: Faker::Number.between(25, 55)) + (0..Faker::Number.between(1, 5)).each do + Child.create(name: Faker::Name.name, parent: parent, age: Faker::Number.between(1, 25)) + end + end end From f5d47e97a19649b06578dcb63cd363ba7a98fc88 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Mon, 15 Jul 2019 16:53:58 -0600 Subject: [PATCH 40/48] wip --- lib/query_helper.rb | 262 +++++++++++------------ lib/query_helper/column_map.rb | 8 +- lib/query_helper/filter.rb | 7 +- lib/query_helper/invalid_query_error.rb | 3 + lib/query_helper/query_helper_concern.rb | 8 +- lib/query_helper/sql_filter.rb | 9 +- lib/query_helper/sql_manipulator.rb | 2 +- lib/query_helper/sql_parser.rb | 1 + lib/query_helper/sql_sort.rb | 6 +- spec/controller_test.rb | 15 +- spec/fixtures/controllers.rb | 1 - spec/spec_helper.rb | 2 - 12 files changed, 175 insertions(+), 149 deletions(-) create mode 100644 lib/query_helper/invalid_query_error.rb diff --git a/lib/query_helper.rb b/lib/query_helper.rb index 09e0fce..1c3b773 100644 --- a/lib/query_helper.rb +++ b/lib/query_helper.rb @@ -9,156 +9,154 @@ require "query_helper/sql_manipulator" require "query_helper/sql_filter" require "query_helper/sql_sort" +require "query_helper/invalid_query_error" class QueryHelper - # class InvalidQueryError < StandardError; end - - # class << self - attr_accessor :active_record_adapter, :model, :query, :bind_variables, :sql_filter, :sql_sort, :page, :per_page, :single_record, :associations, :as_json_options - - def initialize( - model: nil, # the model to run the query against - query: nil, # a sql string or an active record query - bind_variables: {}, # a list of bind variables to be embedded into the query - sql_filter: SqlFilter.new(), # a SqlFilter object - sql_sort: SqlSort.new(), # a SqlSort object - page: nil, # define the page you want returned - per_page: nil, # define how many results you want per page - single_record: false, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned - associations: nil, # a list of activerecord associations you'd like included in the payload - as_json_options: nil, # a list of as_json options you'd like run before returning the payload - custom_mappings: {} # custom keyword => sql_expression mappings - ) - @model = model - @query = query - @bind_variables = bind_variables - @sql_filter = sql_filter - @sql_sort = sql_sort - @page = page.to_i if page - @per_page = per_page.to_i if per_page - @single_record = single_record - @associations = associations - @as_json_options = as_json_options - @custom_mappings = custom_mappings - - if @page && @per_page - # Determine limit and offset - limit = @per_page - offset = (@page - 1) * @per_page - - # Merge limit/offset variables into bind_variables - @bind_variables.merge!({limit: limit, offset: offset}) - end + attr_accessor :model, :query, :bind_variables, :sql_filter, :sql_sort, :page, :per_page, :single_record, :associations, :as_json_options + + def initialize( + model: nil, # the model to run the query against + query: nil, # a sql string or an active record query + bind_variables: {}, # a list of bind variables to be embedded into the query + sql_filter: SqlFilter.new(), # a SqlFilter object + sql_sort: SqlSort.new(), # a SqlSort object + page: nil, # define the page you want returned + per_page: nil, # define how many results you want per page + single_record: false, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned + associations: nil, # a list of activerecord associations you'd like included in the payload + as_json_options: nil, # a list of as_json options you'd like run before returning the payload + custom_mappings: {} # custom keyword => sql_expression mappings + ) + @model = model + @query = query + @bind_variables = bind_variables + @sql_filter = sql_filter + @sql_sort = sql_sort + @page = page.to_i if page + @per_page = per_page.to_i if per_page + @single_record = single_record + @associations = associations + @as_json_options = as_json_options + @custom_mappings = custom_mappings + + if @page && @per_page + # Determine limit and offset + limit = @per_page + offset = (@page - 1) * @per_page + + # Merge limit/offset variables into bind_variables + @bind_variables.merge!({limit: limit, offset: offset}) end + end - def execute_query - puts caller[0][/`.*'/][1..-2] - - # Correctly set the query and model based on query type - determine_query_type() + def execute_query + puts caller[0][/`.*'/][1..-2] - # Create column maps to be used by the filter and sort objects - column_maps = create_column_maps() - @sql_filter.column_maps = column_maps - @sql_sort.column_maps = column_maps + # Correctly set the query and model based on query type + determine_query_type() - # create the filters from the column maps - @sql_filter.create_filters + # Create column maps to be used by the filter and sort objects + column_maps = create_column_maps() + @sql_filter.column_maps = column_maps + @sql_sort.column_maps = column_maps - # merge the filter bind variables into the query bind variables - @bind_variables.merge!(@sql_filter.bind_variables) + # create the filters from the column maps + @sql_filter.create_filters - # Execute Sql Query - manipulator = SqlManipulator.new( - sql: @query, - where_clauses: @sql_filter.where_clauses, - having_clauses: @sql_filter.having_clauses, - order_by_clauses: @sql_sort.parse_sort_string, - include_limit_clause: @page && @per_page ? true : false - ) + # merge the filter bind variables into the query bind variables + @bind_variables.merge!(@sql_filter.bind_variables) - @results = @model.find_by_sql([manipulator.build(), @bind_variables]) # Execute Sql Query - @results = @results.first if @single_record # Return a single result if requested + # Execute Sql Query + manipulator = SqlManipulator.new( + sql: @query, + where_clauses: @sql_filter.where_clauses, + having_clauses: @sql_filter.having_clauses, + order_by_clauses: @sql_sort.parse_sort_string, + include_limit_clause: @page && @per_page ? true : false + ) - determine_count() - load_associations() - clean_results() + @results = @model.find_by_sql([manipulator.build(), @bind_variables]) # Execute Sql Query + @results = @results.first if @single_record # Return a single result if requested + + determine_count() + load_associations() + clean_results() + end + + def results + execute_query() + @results + end + + def paginated_results + execute_query() + { pagination: pagination_results(), + data: @results } + end + + + private + + def determine_query_type + # If a custom sql string is passed in, make sure a valid model is passed in as well + if @query.class == String + raise InvalidQueryError.new("a valid model must be included to run a custom SQL query") unless @model < ActiveRecord::Base + # If an active record query is passed in, find the model and sql from the query + elsif @query.class < ActiveRecord::Relation + @model = @query.model + @query = @query.to_sql + else + raise InvalidQueryError.new("unable to determine query type") + end end - def results - execute_query() - @results + def determine_count + # Determine total result count (unpaginated) + @count = @page && @per_page && @results.length > 0 ? @results.first["_query_full_count"] : @results.length end - def paginated_results - execute_query() - { pagination: pagination_results(), - data: @results } + def load_associations + @results = Associations.load_associations( + payload: @results, + associations: @associations, + as_json_options: @as_json_options + ) end + def clean_results + @results.map!{ |r| r.except("_query_full_count") } if @page && @per_page + end - private - - def determine_query_type - # If a custom sql string is passed in, make sure a valid model is passed in as well - if @query.class == String - raise InvalidQueryError.new("a valid model must be included to run a custom SQL query") unless @model < ActiveRecord::Base - # If an active record query is passed in, find the model and sql from the query - elsif @query.class < ActiveRecord::Relation - @model = @query.model - @query = @query.to_sql - else - raise InvalidQueryError.new("unable to determine query type") - end - end - - def determine_count - # Determine total result count (unpaginated) - @count = @page && @per_page && @results.length > 0 ? @results.first["_query_full_count"] : @results.length - end - - def load_associations - @results = Associations.load_associations( - payload: @results, - associations: @associations, - as_json_options: @as_json_options - ) - end - - def clean_results - @results.map!{ |r| r.except("_query_full_count") } if @page && @per_page - end - - def pagination_results - # Set pagination params if they aren't provided - @per_page = @count unless @per_page - @page = 1 unless @page - - total_pages = (@count/(@per_page.nonzero? || 1).to_f).ceil - next_page = @page + 1 if @page.between?(1, total_pages - 1) - previous_page = @page - 1 if @page.between?(2, total_pages) - first_page = @page == 1 - last_page = @page == total_pages - out_of_range = !@page.between?(1,total_pages) - - { count: @count, - current_page: @page, - next_page: next_page, - previous_page: previous_page, - total_pages: total_pages, - per_page: @per_page, - first_page: first_page, - last_page: last_page, - out_of_range: out_of_range } - end + def pagination_results + # Set pagination params if they aren't provided + @per_page = @count unless @per_page + @page = 1 unless @page + + total_pages = (@count/(@per_page.nonzero? || 1).to_f).ceil + next_page = @page + 1 if @page.between?(1, total_pages - 1) + previous_page = @page - 1 if @page.between?(2, total_pages) + first_page = @page == 1 + last_page = @page == total_pages + out_of_range = !@page.between?(1,total_pages) + + { count: @count, + current_page: @page, + next_page: next_page, + previous_page: previous_page, + total_pages: total_pages, + per_page: @per_page, + first_page: first_page, + last_page: last_page, + out_of_range: out_of_range } + end - def create_column_maps - ColumnMap.create_column_mappings( - query: @query, - custom_mappings: @custom_mappings - ) - end + def create_column_maps + ColumnMap.create_column_mappings( + query: @query, + custom_mappings: @custom_mappings, + model: @model + ) + end - # end end diff --git a/lib/query_helper/column_map.rb b/lib/query_helper/column_map.rb index 2eb05ef..caa17f3 100644 --- a/lib/query_helper/column_map.rb +++ b/lib/query_helper/column_map.rb @@ -3,7 +3,7 @@ class QueryHelper class ColumnMap - def self.create_column_mappings(custom_mappings:, query:) + def self.create_column_mappings(custom_mappings:, query:, model:) parser = SqlParser.new(query) maps = create_from_hash(custom_mappings) @@ -11,6 +11,12 @@ def self.create_column_mappings(custom_mappings:, query:) maps << m if maps.select{|x| x.alias_name == m.alias_name}.empty? end + model.attribute_names.each do |attribute| + if maps.select{|x| x.alias_name == attribute}.empty? + maps << ColumnMap.new(alias_name: attribute, sql_expression: "#{model.to_s.downcase.pluralize}.#{attribute}") + end + end + maps end diff --git a/lib/query_helper/filter.rb b/lib/query_helper/filter.rb index 04e6cb5..69cd019 100644 --- a/lib/query_helper/filter.rb +++ b/lib/query_helper/filter.rb @@ -1,12 +1,15 @@ +require "query_helper/invalid_query_error" + class QueryHelper class Filter - attr_accessor :operator, :criterion, :comparate, :operator_code, :bind_variable + attr_accessor :operator, :criterion, :comparate, :operator_code, :bind_variable, :aggregate def initialize( operator_code:, criterion:, - comparate: + comparate:, + aggregate: false ) @operator_code = operator_code @criterion = criterion # Converts to a string to be inserted into sql. diff --git a/lib/query_helper/invalid_query_error.rb b/lib/query_helper/invalid_query_error.rb new file mode 100644 index 0000000..a394eba --- /dev/null +++ b/lib/query_helper/invalid_query_error.rb @@ -0,0 +1,3 @@ +class QueryHelper + class InvalidQueryError < StandardError; end +end diff --git a/lib/query_helper/query_helper_concern.rb b/lib/query_helper/query_helper_concern.rb index de81e6d..b8bb015 100644 --- a/lib/query_helper/query_helper_concern.rb +++ b/lib/query_helper/query_helper_concern.rb @@ -15,11 +15,11 @@ def create_query_helper end def create_query_helper_filter - + QueryHelper::SqlFilter.new(filter_values: params[:filter]) end def create_query_helper_sort - + QueryHelper::SqlSort.new(sort_string: params[:sort]) end def create_query_helper_associations @@ -30,8 +30,8 @@ def query_helper_params helpers = {} helpers[:page] = params[:page] if params[:page] helpers[:per_page] = params[:per_page] if params[:per_page] - helpers[:filters] = create_query_helper_filter() if params[:filter] - helpers[:sorts] = create_query_helper_sort() if params[:sort] + helpers[:sql_filter] = create_query_helper_filter() if params[:filter] + helpers[:sql_sort] = create_query_helper_sort() if params[:sort] helpers[:associations] = create_query_helper_associations() if params[:include] helpers end diff --git a/lib/query_helper/sql_filter.rb b/lib/query_helper/sql_filter.rb index 00a3d6d..a557ea4 100644 --- a/lib/query_helper/sql_filter.rb +++ b/lib/query_helper/sql_filter.rb @@ -1,9 +1,11 @@ +require "query_helper/invalid_query_error" + class QueryHelper class SqlFilter attr_accessor :filter_values, :column_maps - def initialize(filter_values: [], column_maps: nil) + def initialize(filter_values: [], column_maps: []) @column_maps = column_maps @filter_values = filter_values end @@ -14,13 +16,14 @@ def create_filters @filter_values.each do |comparate_alias, criteria| # Find the sql mapping if it exists map = @column_maps.find { |m| m.alias_name == comparate_alias } - raise InvalidQueryError("cannot filter by #{comparate_alias}") unless map + raise InvalidQueryError.new("cannot filter by #{comparate_alias}") unless map # create the filter @filters << QueryHelper::Filter.new( operator_code: criteria.keys.first, criterion: criteria.values.first, - comparate: map.sql_expression + comparate: map.sql_expression, + aggregate: map.aggregate ) end end diff --git a/lib/query_helper/sql_manipulator.rb b/lib/query_helper/sql_manipulator.rb index 04130fe..10f8036 100644 --- a/lib/query_helper/sql_manipulator.rb +++ b/lib/query_helper/sql_manipulator.rb @@ -21,8 +21,8 @@ def initialize( end def build - insert_limit_clause() insert_order_by_clause() + insert_limit_clause() insert_having_clauses() insert_where_clauses() insert_total_count_select_clause() diff --git a/lib/query_helper/sql_parser.rb b/lib/query_helper/sql_parser.rb index 4211297..6b068e9 100644 --- a/lib/query_helper/sql_parser.rb +++ b/lib/query_helper/sql_parser.rb @@ -1,3 +1,4 @@ +require "query_helper/invalid_query_error" require "query_helper/column_map" class QueryHelper diff --git a/lib/query_helper/sql_sort.rb b/lib/query_helper/sql_sort.rb index f820894..78dd710 100644 --- a/lib/query_helper/sql_sort.rb +++ b/lib/query_helper/sql_sort.rb @@ -1,3 +1,5 @@ +require "query_helper/invalid_query_error" + class QueryHelper class SqlSort @@ -23,8 +25,8 @@ def parse_sort_string end if direction == "desc" - case QueryHelper.active_record_adapter - when "sqlite3" # sqlite3 is used in the test suite + case ActiveRecord::Base.connection.adapter_name + when "SQLite" # SQLite is used in the test suite direction = "desc" else direction = "desc nulls last" diff --git a/spec/controller_test.rb b/spec/controller_test.rb index b60a6f0..dd3d4a0 100644 --- a/spec/controller_test.rb +++ b/spec/controller_test.rb @@ -5,8 +5,21 @@ RSpec.describe ParentsController, type: :controller do describe '#index' do + url_params = { + filter: { + "id" => { + "gte" => 20, + "lt" => 40 + } + }, + page: Faker::Number.between(5,15).to_s, + per_page: Faker::Number.between(2,5).to_s, + sort: "name:desc", + include: "parent" + } + it "text" do - get :index, params: {hello: "world"} + get :index, params: url_params byebug end end diff --git a/spec/fixtures/controllers.rb b/spec/fixtures/controllers.rb index 77fa150..572a73c 100644 --- a/spec/fixtures/controllers.rb +++ b/spec/fixtures/controllers.rb @@ -10,7 +10,6 @@ class ApplicationController < ActionController::API class ParentsController < ApplicationController def index @query_helper.query = Parent.all - byebug render json: @query_helper.paginated_results() end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 256328b..bcca9a1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -37,8 +37,6 @@ } end - # QueryHelper.active_record_adapter = "sqlite3" - # Set up a database that resides in RAM ActiveRecord::Base.establish_connection( adapter: "sqlite3", From 3ee8926e80eec27aa4131db1e439e474ce7cf63a Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 16 Jul 2019 10:35:53 -0600 Subject: [PATCH 41/48] updates --- lib/query_helper.rb | 8 +- lib/query_helper/sql_manipulator.rb | 2 +- lib/query_helper/sql_parser.rb | 2 +- spec/controller_test.rb | 71 +++++++--- spec/fixtures/application.rb | 6 +- spec/fixtures/controllers.rb | 8 +- spec/fixtures/example_queries.rb | 203 ++++++++++++++++++++++++++++ 7 files changed, 275 insertions(+), 25 deletions(-) create mode 100644 spec/fixtures/example_queries.rb diff --git a/lib/query_helper.rb b/lib/query_helper.rb index 1c3b773..76442bc 100644 --- a/lib/query_helper.rb +++ b/lib/query_helper.rb @@ -13,7 +13,7 @@ class QueryHelper - attr_accessor :model, :query, :bind_variables, :sql_filter, :sql_sort, :page, :per_page, :single_record, :associations, :as_json_options + attr_accessor :model, :query, :bind_variables, :sql_filter, :sql_sort, :page, :per_page, :single_record, :associations, :as_json_options, :executed_query def initialize( model: nil, # the model to run the query against @@ -51,8 +51,6 @@ def initialize( end def execute_query - puts caller[0][/`.*'/][1..-2] - # Correctly set the query and model based on query type determine_query_type() @@ -75,8 +73,8 @@ def execute_query order_by_clauses: @sql_sort.parse_sort_string, include_limit_clause: @page && @per_page ? true : false ) - - @results = @model.find_by_sql([manipulator.build(), @bind_variables]) # Execute Sql Query + @executed_query = manipulator.build() + @results = @model.find_by_sql([@executed_query, @bind_variables]) # Execute Sql Query @results = @results.first if @single_record # Return a single result if requested determine_count() diff --git a/lib/query_helper/sql_manipulator.rb b/lib/query_helper/sql_manipulator.rb index 10f8036..043dcd5 100644 --- a/lib/query_helper/sql_manipulator.rb +++ b/lib/query_helper/sql_manipulator.rb @@ -55,13 +55,13 @@ def insert_having_clauses def insert_order_by_clause return unless @order_by_clauses.length > 0 + @sql.slice!(@parser.limit_clause) if @parser.limit_included? # remove existing limit clause @sql.slice!(@parser.order_by_clause) if @parser.order_by_included? # remove existing order by clause @sql.insert(@parser.insert_having_index, " order by #{@order_by_clauses.join(", ")} ") end def insert_limit_clause return unless @include_limit_clause - @sql.slice!(@parser.limit_clause) if @parser.limit_included? # remove existing limit clause @sql.insert(@parser.insert_limit_index, " limit :limit offset :offset ") end end diff --git a/lib/query_helper/sql_parser.rb b/lib/query_helper/sql_parser.rb index 6b068e9..5db6bfa 100644 --- a/lib/query_helper/sql_parser.rb +++ b/lib/query_helper/sql_parser.rb @@ -160,7 +160,7 @@ def find_aliases comma_split_points.unshift(-1) # We need the first select clause to start out with a 'split' column_maps = white_out_selects.split(",").each_with_index.map do |x,i| sql_alias = x.squish.split(" as ")[1] || x.squish.split(" AS ")[1] || x.squish.split(".")[1] # look for custom defined aliases or table.column notation - sql_alias = nil unless /^[a-zA-Z_]+$/.match?(sql_alias) # only allow aliases with letters and underscores + # sql_alias = nil unless /^[a-zA-Z_]+$/.match?(sql_alias) # only allow aliases with letters and underscores sql_expression = if x.split(" as ")[1] expression_length = x.split(" as ")[0].length selects[comma_split_points[i] + 1, expression_length] diff --git a/spec/controller_test.rb b/spec/controller_test.rb index dd3d4a0..3f502d9 100644 --- a/spec/controller_test.rb +++ b/spec/controller_test.rb @@ -1,26 +1,65 @@ require 'fixtures/application' require 'fixtures/controllers' require 'fixtures/models' +require 'fixtures/example_queries' require 'rspec/rails' RSpec.describe ParentsController, type: :controller do describe '#index' do - url_params = { - filter: { - "id" => { - "gte" => 20, - "lt" => 40 - } - }, - page: Faker::Number.between(5,15).to_s, - per_page: Faker::Number.between(2,5).to_s, - sort: "name:desc", - include: "parent" - } - - it "text" do - get :index, params: url_params - byebug + # url_params = { + # filter: { + # "id" => { + # "gte" => 20, + # "lt" => 40 + # } + # }, + # page: Faker::Number.between(5,15).to_s, + # per_page: Faker::Number.between(2,5).to_s, + # sort: "name:desc", + # include: "parent" + # } + # + # it "text" do + # get :index, params: url_params + # byebug + # end + + it "test example queries" do + ExampleQueries::SQL_QUERIES.each_with_index do |q, index| + + q[:expected_sorts].each do |sort| + sort_direction = ["asc","desc"].sample + + url_params = { + page: Faker::Number.between(5,15).to_s, + per_page: Faker::Number.between(2,5).to_s, + sort: "#{sort}:#{sort_direction}", + test_number: index + } + + puts "INDEX: #{index} --- QUERY: #{q[:query].squish} --- SORT: #{sort}" + + get :test, params: url_params + + rsp = JSON.parse(response.body) + data = rsp["data"] + pagination = rsp["pagination"] + + previous_value = nil + data.each_with_index do |d, index| + if index > 0 + if sort_direction == "desc" + expect(d[sort] <= previous_value).to eql(true) + else + expect(d[sort] >= previous_value).to eql(true) + end + end + previous_value = d[sort] + end + + end + + end end end end diff --git a/spec/fixtures/application.rb b/spec/fixtures/application.rb index 5f02659..58e3c46 100644 --- a/spec/fixtures/application.rb +++ b/spec/fixtures/application.rb @@ -10,7 +10,11 @@ def routes return @routes if defined?(@routes) @routes = ActionDispatch::Routing::RouteSet.new @routes.draw do - resources :parents + resources :parents do + collection do + get 'test' + end + end end @routes end diff --git a/spec/fixtures/controllers.rb b/spec/fixtures/controllers.rb index 572a73c..c9779ba 100644 --- a/spec/fixtures/controllers.rb +++ b/spec/fixtures/controllers.rb @@ -13,6 +13,12 @@ def index render json: @query_helper.paginated_results() end - def show + def test + test_query = ExampleQueries::SQL_QUERIES[params[:test_number].to_i] + @query_helper.query = test_query[:query] + @query_helper.model = test_query[:model] + results = @query_helper.paginated_results() + puts "EXECUTED QUERY: #{@query_helper.executed_query()}" + render json: @query_helper.paginated_results() end end diff --git a/spec/fixtures/example_queries.rb b/spec/fixtures/example_queries.rb new file mode 100644 index 0000000..d96b08f --- /dev/null +++ b/spec/fixtures/example_queries.rb @@ -0,0 +1,203 @@ +module ExampleQueries + SQL_QUERIES = [ + # { + # query: %( + # select c.id, c.name, c.age + # from children c + # ), + # model: Child, + # expected_sorts: ["id", "name", "age"], + # expected_filters: [ + # { + # alias: "id", + # operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], + # class: Integer + # }, + # { + # alias: "age", + # operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], + # class: Integer + # }, + # { + # alias: "name", + # operator_codes: ["like"], + # class: String + # }, + # { + # alias: "name", + # operator_codes: ["in, not_in"], + # class: Array + # }, + # { + # alias: "age", + # operator_codes: ["null"], + # class: TrueClass + # }, + # ] + # }, + # { + # query: %( + # select p.name, p.age, count(c.id) as children_count + # from parents p join children c on c.parent_id = p.id + # where p.age > 35 + # group by p.name + # having count(c.id) > 3 + # order by p.name + # limit 1 + # ), + # model: Child, + # expected_sorts: ["name", "age", "children_count"], + # expected_filters: [ + # { + # alias: "id", + # operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], + # class: Integer + # }, + # { + # alias: "age", + # operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], + # class: Integer + # }, + # { + # alias: "name", + # operator_codes: ["like"], + # class: String + # }, + # { + # alias: "name", + # operator_codes: ["in, not_in"], + # class: Array + # }, + # { + # alias: "age", + # operator_codes: ["null"], + # class: TrueClass + # }, + # { + # alias: "children_count", + # operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], + # class: Integer + # }, + # ] + # }, + # { + # query: %( + # with children_count as ( + # select p.id as parent_id, p.age, count(c.id) as children_count + # from parents p join children c on c.parent_id = p.id + # group by p.id + # ), + # max_age as ( + # select p.id as parent_id, max(c.age) as max_age + # from parents p join children c on c.parent_id = p.id + # group by p.id + # ) + # select p.name, c.children_count, m.max_age + # from parents p + # join children_count c on p.id = c.parent_id + # join max_age m on p.id = m.parent_id + # ), + # model: Child, + # expected_sorts: ["name", "children_count", "max_age"] + # }, + # { + # query: %( + # select + # p.name, + # ( + # select max(c2.age) + # from parents p2 join children c2 on c2.parent_id = p2.id + # where p2.id = p.id + # group by p2.id + # ) as max_age + # from parents p + # ), + # model: Child, + # expected_sorts: ["name", "max_age"] + # }, + # { + # query: %( + # select c.id, c.name, c.age, p.name as parent_name + # from children c + # join parents p on p.id = c.parent_id + # ), + # model: Child, + # expected_sorts: ["id", "name", "age", "parent_name"] + # }, + { + query: %( + select + p.name as parent_name, + count(c.id) as count, + COUNT(c.age) as count2, + -- array_agg(c.name) as children_names, + -- ARRAY_AGG(c.name) as children_names2, + avg(c.age) as average, + AVG(c.id) as average2, + -- bit_and(c.age) as bit_and, + -- BIT_AND(c.age) as bit_and2, + -- bit_or(c.age) as bit_or, + -- BIT_OR(c.age) as bit_or2, + -- bool_and(true) as bool_and, + -- BOOL_AND(true) as bool_and2, + -- bool_or(true) as bool_or, + -- BOOL_OR(true) as bool_or2, + -- every(false) as every, + -- EVERY(false) as every2, + -- json_agg(c.*) as json_agg, + -- JSON_AGG(c.*) as json_agg2, + -- jsonb_agg(c.*) as jsonb_agg, + -- JSONB_AGG(c.*) as jsonb_agg2, + -- json_object_agg(c.*) as json_object_agg, + -- JSON_OBJECT_AGG(c.*) as json_object_agg2, + -- jsonb_object_agg(c.*) as jsonb_object_agg, + -- JSONB_OBJECT_AGG(c.*) as jsonb_object_agg2, + max(c.age) as max, + MAX(c.id) as max2, + min(c.age) as min, + MIN(c.id) as min2, + sum(c.age) as sum, + SUM(c.id) as sum2 + from children c + join parents p on p.id = c.parent_id + group by p.name + ), + model: Child, + expected_sorts: [ + "parent_name", + "count", + "count2", + # "children_names", + # "children_names2", + "average", + "average2", + # "bit_and", + # "bit_and2", + # "bit_or", + # "bit_or2", + # "bool_and", + # "bool_and2", + # "bool_or", + # "bool_or2", + # "every", + # "every2", + # "every2", + # "json_agg", + # "json_agg2", + # "jsonb_agg", + # "jsonb_agg2", + # "json_object_agg", + # "json_object_agg2", + # "jsonb_object_agg", + # "jsonb_object_agg2", + "max", + "max2", + "min", + "min2", + "sum", + "sum2" + ] + }, + ] + +end From 32f91cbb2880a9bae20cce3735de22f73e669e79 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 16 Jul 2019 11:07:41 -0600 Subject: [PATCH 42/48] uncomment some tests --- spec/fixtures/example_queries.rb | 248 +++++++++++++++---------------- 1 file changed, 124 insertions(+), 124 deletions(-) diff --git a/spec/fixtures/example_queries.rb b/spec/fixtures/example_queries.rb index d96b08f..e98f18c 100644 --- a/spec/fixtures/example_queries.rb +++ b/spec/fixtures/example_queries.rb @@ -1,129 +1,129 @@ module ExampleQueries SQL_QUERIES = [ - # { - # query: %( - # select c.id, c.name, c.age - # from children c - # ), - # model: Child, - # expected_sorts: ["id", "name", "age"], - # expected_filters: [ - # { - # alias: "id", - # operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], - # class: Integer - # }, - # { - # alias: "age", - # operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], - # class: Integer - # }, - # { - # alias: "name", - # operator_codes: ["like"], - # class: String - # }, - # { - # alias: "name", - # operator_codes: ["in, not_in"], - # class: Array - # }, - # { - # alias: "age", - # operator_codes: ["null"], - # class: TrueClass - # }, - # ] - # }, - # { - # query: %( - # select p.name, p.age, count(c.id) as children_count - # from parents p join children c on c.parent_id = p.id - # where p.age > 35 - # group by p.name - # having count(c.id) > 3 - # order by p.name - # limit 1 - # ), - # model: Child, - # expected_sorts: ["name", "age", "children_count"], - # expected_filters: [ - # { - # alias: "id", - # operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], - # class: Integer - # }, - # { - # alias: "age", - # operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], - # class: Integer - # }, - # { - # alias: "name", - # operator_codes: ["like"], - # class: String - # }, - # { - # alias: "name", - # operator_codes: ["in, not_in"], - # class: Array - # }, - # { - # alias: "age", - # operator_codes: ["null"], - # class: TrueClass - # }, - # { - # alias: "children_count", - # operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], - # class: Integer - # }, - # ] - # }, - # { - # query: %( - # with children_count as ( - # select p.id as parent_id, p.age, count(c.id) as children_count - # from parents p join children c on c.parent_id = p.id - # group by p.id - # ), - # max_age as ( - # select p.id as parent_id, max(c.age) as max_age - # from parents p join children c on c.parent_id = p.id - # group by p.id - # ) - # select p.name, c.children_count, m.max_age - # from parents p - # join children_count c on p.id = c.parent_id - # join max_age m on p.id = m.parent_id - # ), - # model: Child, - # expected_sorts: ["name", "children_count", "max_age"] - # }, - # { - # query: %( - # select - # p.name, - # ( - # select max(c2.age) - # from parents p2 join children c2 on c2.parent_id = p2.id - # where p2.id = p.id - # group by p2.id - # ) as max_age - # from parents p - # ), - # model: Child, - # expected_sorts: ["name", "max_age"] - # }, - # { - # query: %( - # select c.id, c.name, c.age, p.name as parent_name - # from children c - # join parents p on p.id = c.parent_id - # ), - # model: Child, - # expected_sorts: ["id", "name", "age", "parent_name"] - # }, + { + query: %( + select c.id, c.name, c.age + from children c + ), + model: Child, + expected_sorts: ["id", "name", "age"], + expected_filters: [ + { + alias: "id", + operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], + class: Integer + }, + { + alias: "age", + operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], + class: Integer + }, + { + alias: "name", + operator_codes: ["like"], + class: String + }, + { + alias: "name", + operator_codes: ["in, not_in"], + class: Array + }, + { + alias: "age", + operator_codes: ["null"], + class: TrueClass + }, + ] + }, + { + query: %( + select p.name, p.age, count(c.id) as children_count + from parents p join children c on c.parent_id = p.id + where p.age > 35 + group by p.name + having count(c.id) > 3 + order by p.name + limit 1 + ), + model: Child, + expected_sorts: ["name", "age", "children_count"], + expected_filters: [ + { + alias: "id", + operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], + class: Integer + }, + { + alias: "age", + operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], + class: Integer + }, + { + alias: "name", + operator_codes: ["like"], + class: String + }, + { + alias: "name", + operator_codes: ["in, not_in"], + class: Array + }, + { + alias: "age", + operator_codes: ["null"], + class: TrueClass + }, + { + alias: "children_count", + operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], + class: Integer + }, + ] + }, + { + query: %( + with children_count as ( + select p.id as parent_id, p.age, count(c.id) as children_count + from parents p join children c on c.parent_id = p.id + group by p.id + ), + max_age as ( + select p.id as parent_id, max(c.age) as max_age + from parents p join children c on c.parent_id = p.id + group by p.id + ) + select p.name, c.children_count, m.max_age + from parents p + join children_count c on p.id = c.parent_id + join max_age m on p.id = m.parent_id + ), + model: Child, + expected_sorts: ["name", "children_count", "max_age"] + }, + { + query: %( + select + p.name, + ( + select max(c2.age) + from parents p2 join children c2 on c2.parent_id = p2.id + where p2.id = p.id + group by p2.id + ) as max_age + from parents p + ), + model: Child, + expected_sorts: ["name", "max_age"] + }, + { + query: %( + select c.id, c.name, c.age, p.name as parent_name + from children c + join parents p on p.id = c.parent_id + ), + model: Child, + expected_sorts: ["id", "name", "age", "parent_name"] + }, { query: %( select From 82caf32b5cf2995bba99b2c1d201ee57e9b12e58 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 16 Jul 2019 11:15:40 -0600 Subject: [PATCH 43/48] wip --- spec/controller_test.rb | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/spec/controller_test.rb b/spec/controller_test.rb index 3f502d9..ffc5395 100644 --- a/spec/controller_test.rb +++ b/spec/controller_test.rb @@ -59,6 +59,39 @@ end + # q[:expected_filters].each do |filter| + # # filter = { + # # "id" => { + # # "gte" => 20, + # # "lt" => 40 + # # } + # # }, + # filter[:operator_codes].each do |oc| + # filter_value = case filter[:class] + # when Integer + # Faker::Number.between(0,100).to_s + # when String + # case oc + # when "in", "notin" + # + # end + # end + # filter = { + # filter[:alias] => { + # oc => filter_value + # } + # } + # end + # url_params = { + # page: Faker::Number.between(5,15).to_s, + # per_page: Faker::Number.between(2,5).to_s, + # sort: "#{sort}:#{sort_direction}", + # test_number: index + # } + # + # puts "INDEX: #{index} --- QUERY: #{q[:query].squish} --- filter: #{filter}" + # end if q[:expected_filters] + end end end From 579efbf7b0dd2e75f89fa2544e68a8577061ae9c Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Fri, 19 Jul 2019 12:28:25 -0600 Subject: [PATCH 44/48] Fix filtering --- lib/query_helper.rb | 29 ++++++++++++++++-------- lib/query_helper/filter.rb | 1 + lib/query_helper/query_helper_concern.rb | 5 ++-- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/query_helper.rb b/lib/query_helper.rb index 76442bc..cb3a662 100644 --- a/lib/query_helper.rb +++ b/lib/query_helper.rb @@ -13,7 +13,7 @@ class QueryHelper - attr_accessor :model, :query, :bind_variables, :sql_filter, :sql_sort, :page, :per_page, :single_record, :associations, :as_json_options, :executed_query + attr_accessor :model, :query, :bind_variables, :sql_filter, :sql_sort, :page, :per_page, :single_record, :associations, :as_json_options, :executed_query, :api_payload def initialize( model: nil, # the model to run the query against @@ -26,7 +26,8 @@ def initialize( single_record: false, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned associations: nil, # a list of activerecord associations you'd like included in the payload as_json_options: nil, # a list of as_json options you'd like run before returning the payload - custom_mappings: {} # custom keyword => sql_expression mappings + custom_mappings: {}, # custom keyword => sql_expression mappings + api_payload: false # Return the paginated payload or simply return the result array ) @model = model @query = query @@ -39,6 +40,7 @@ def initialize( @associations = associations @as_json_options = as_json_options @custom_mappings = custom_mappings + @api_payload = api_payload if @page && @per_page # Determine limit and offset @@ -50,6 +52,12 @@ def initialize( end end + def update_query(query: nil, model:nil, bind_variables: {}) + @model = model if model + @query = query if query + @bind_variables.merge!(bind_variables) + end + def execute_query # Correctly set the query and model based on query type determine_query_type() @@ -60,7 +68,7 @@ def execute_query @sql_sort.column_maps = column_maps # create the filters from the column maps - @sql_filter.create_filters + @sql_filter.create_filters() # merge the filter bind variables into the query bind variables @bind_variables.merge!(@sql_filter.bind_variables) @@ -82,20 +90,21 @@ def execute_query clean_results() end - def results + def results() execute_query() - @results + return paginated_results() if @api_payload + return @results end - def paginated_results - execute_query() - { pagination: pagination_results(), - data: @results } - end private + def paginated_results + { pagination: pagination_results(), + data: @results } + end + def determine_query_type # If a custom sql string is passed in, make sure a valid model is passed in as well if @query.class == String diff --git a/lib/query_helper/filter.rb b/lib/query_helper/filter.rb index 69cd019..843b40e 100644 --- a/lib/query_helper/filter.rb +++ b/lib/query_helper/filter.rb @@ -14,6 +14,7 @@ def initialize( @operator_code = operator_code @criterion = criterion # Converts to a string to be inserted into sql. @comparate = comparate + @aggregate = aggregate @bind_variable = ('a'..'z').to_a.shuffle[0,20].join.to_sym translate_operator_code() diff --git a/lib/query_helper/query_helper_concern.rb b/lib/query_helper/query_helper_concern.rb index b8bb015..04094c7 100644 --- a/lib/query_helper/query_helper_concern.rb +++ b/lib/query_helper/query_helper_concern.rb @@ -11,11 +11,12 @@ def query_helper end def create_query_helper - @query_helper = QueryHelper.new(**query_helper_params) + @query_helper = QueryHelper.new(**query_helper_params, api_payload: true) end def create_query_helper_filter - QueryHelper::SqlFilter.new(filter_values: params[:filter]) + filter_values = params[:filter].permit!.to_h + QueryHelper::SqlFilter.new(filter_values: filter_values) end def create_query_helper_sort From c86d306c191c3b1823cc00a8df9516ac33af0ef3 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Fri, 19 Jul 2019 15:30:36 -0600 Subject: [PATCH 45/48] updates --- lib/query_helper/filter.rb | 2 +- lib/query_helper/sql_manipulator.rb | 18 +++----- lib/query_helper/sql_parser.rb | 12 ++--- spec/controller_test.rb | 71 ++++++++++++++++------------- spec/fixtures/controllers.rb | 6 +-- spec/fixtures/example_queries.rb | 17 +++---- 6 files changed, 61 insertions(+), 65 deletions(-) diff --git a/lib/query_helper/filter.rb b/lib/query_helper/filter.rb index 843b40e..8094560 100644 --- a/lib/query_helper/filter.rb +++ b/lib/query_helper/filter.rb @@ -106,7 +106,7 @@ def validate_criterion end def invalid_criterion_error - raise InvalidQueryError.new("'#{criterion}' is not a valid criterion for the '#{operator}' operator") + raise InvalidQueryError.new("'#{criterion}' is not a valid criterion for the '#{@operator}' operator") end end end diff --git a/lib/query_helper/sql_manipulator.rb b/lib/query_helper/sql_manipulator.rb index 043dcd5..dc352d1 100644 --- a/lib/query_helper/sql_manipulator.rb +++ b/lib/query_helper/sql_manipulator.rb @@ -13,7 +13,7 @@ def initialize( include_limit_clause: false ) @parser = SqlParser.new(sql) - @sql = @parser.sql + @sql = @parser.sql.dup @where_clauses = where_clauses @having_clauses = having_clauses @order_by_clauses = order_by_clauses @@ -21,12 +21,11 @@ def initialize( end def build - insert_order_by_clause() - insert_limit_clause() insert_having_clauses() insert_where_clauses() insert_total_count_select_clause() - @sql + insert_order_by_and_limit_clause() + @sql.squish end private @@ -53,16 +52,11 @@ def insert_having_clauses @sql.insert(@parser.insert_having_index, " #{begin_string} #{filter_string} ") end - def insert_order_by_clause - return unless @order_by_clauses.length > 0 + def insert_order_by_and_limit_clause @sql.slice!(@parser.limit_clause) if @parser.limit_included? # remove existing limit clause @sql.slice!(@parser.order_by_clause) if @parser.order_by_included? # remove existing order by clause - @sql.insert(@parser.insert_having_index, " order by #{@order_by_clauses.join(", ")} ") - end - - def insert_limit_clause - return unless @include_limit_clause - @sql.insert(@parser.insert_limit_index, " limit :limit offset :offset ") + @sql += " order by #{@order_by_clauses.join(", ")} " if @order_by_clauses.length > 0 + @sql += " limit :limit offset :offset " if @include_limit_clause end end end diff --git a/lib/query_helper/sql_parser.rb b/lib/query_helper/sql_parser.rb index 5db6bfa..65c0864 100644 --- a/lib/query_helper/sql_parser.rb +++ b/lib/query_helper/sql_parser.rb @@ -124,15 +124,15 @@ def insert_limit_index end def select_clause - @sql[select_index()..insert_select_index()] if select_included? + @sql[select_index()..insert_select_index()].strip if select_included? end def from_clause - @sql[from_index()..insert_join_index()] if from_included? + @sql[from_index()..insert_join_index()].strip if from_included? end def where_clause - @sql[where_index()..insert_where_index()] if where_included? + @sql[where_index()..insert_where_index()].strip if where_included? end # def group_by_clause @@ -140,15 +140,15 @@ def where_clause # end def having_clause - @sql[having_index()..insert_having_index()] if having_included? + @sql[having_index()..insert_having_index()].strip if having_included? end def order_by_clause - @sql[order_by_index()..insert_order_by_index()] if order_by_included? + @sql[order_by_index()..insert_order_by_index()].strip if order_by_included? end def limit_clause - @sql[limit_index()..insert_limit_index()] if limit_included? + @sql[limit_index()..insert_limit_index()].strip if limit_included? end def find_aliases diff --git a/spec/controller_test.rb b/spec/controller_test.rb index ffc5395..6c84e8d 100644 --- a/spec/controller_test.rb +++ b/spec/controller_test.rb @@ -59,38 +59,45 @@ end - # q[:expected_filters].each do |filter| - # # filter = { - # # "id" => { - # # "gte" => 20, - # # "lt" => 40 - # # } - # # }, - # filter[:operator_codes].each do |oc| - # filter_value = case filter[:class] - # when Integer - # Faker::Number.between(0,100).to_s - # when String - # case oc - # when "in", "notin" - # - # end - # end - # filter = { - # filter[:alias] => { - # oc => filter_value - # } - # } - # end - # url_params = { - # page: Faker::Number.between(5,15).to_s, - # per_page: Faker::Number.between(2,5).to_s, - # sort: "#{sort}:#{sort_direction}", - # test_number: index - # } - # - # puts "INDEX: #{index} --- QUERY: #{q[:query].squish} --- filter: #{filter}" - # end if q[:expected_filters] + q[:expected_filters].each do |filter| + filter[:operator_codes].each do |oc| + filter_value = if filter[:class] == Integer + Faker::Number.between(0,100).to_s + elsif filter[:class] == String + case oc + when "in", "notin" + "#{q[:model].all.pluck(:name).sample},#{q[:model].all.pluck(:name).sample},#{q[:model].all.pluck(:name).sample}" + when "like" + q[:model].all.pluck(:name).sample[0..4] + end + elsif filter[:class] == TrueClass + case oc + when "null" + ['true', 'false'].sample + end + end + filter_url_param = { + filter[:alias] => { + oc => filter_value + } + } + + url_params = { + page: Faker::Number.between(5,15).to_s, + per_page: Faker::Number.between(2,5).to_s, + filter: filter_url_param, + test_number: index + } + + puts "INDEX: #{index} --- QUERY: #{q[:query].squish} --- filter: #{filter_url_param}" + + get :test, params: url_params + + # TODO: Add some sort of expectation + # Perhaps expect the result to be filtered (have less values than previously) + + end + end if q[:expected_filters] end end diff --git a/spec/fixtures/controllers.rb b/spec/fixtures/controllers.rb index c9779ba..7b0bac3 100644 --- a/spec/fixtures/controllers.rb +++ b/spec/fixtures/controllers.rb @@ -10,15 +10,15 @@ class ApplicationController < ActionController::API class ParentsController < ApplicationController def index @query_helper.query = Parent.all - render json: @query_helper.paginated_results() + render json: @query_helper.results() end def test test_query = ExampleQueries::SQL_QUERIES[params[:test_number].to_i] @query_helper.query = test_query[:query] @query_helper.model = test_query[:model] - results = @query_helper.paginated_results() + results = @query_helper.results() puts "EXECUTED QUERY: #{@query_helper.executed_query()}" - render json: @query_helper.paginated_results() + render json: @query_helper.results() end end diff --git a/spec/fixtures/example_queries.rb b/spec/fixtures/example_queries.rb index e98f18c..025b201 100644 --- a/spec/fixtures/example_queries.rb +++ b/spec/fixtures/example_queries.rb @@ -25,8 +25,8 @@ module ExampleQueries }, { alias: "name", - operator_codes: ["in, not_in"], - class: Array + operator_codes: ["in", "notin"], + class: String }, { alias: "age", @@ -39,20 +39,15 @@ module ExampleQueries query: %( select p.name, p.age, count(c.id) as children_count from parents p join children c on c.parent_id = p.id - where p.age > 35 + where p.age > 30 group by p.name having count(c.id) > 3 order by p.name limit 1 ), - model: Child, + model: Parent, expected_sorts: ["name", "age", "children_count"], expected_filters: [ - { - alias: "id", - operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], - class: Integer - }, { alias: "age", operator_codes: ["gte", "gt", "lte", "lt", "eql", "noteql"], @@ -65,8 +60,8 @@ module ExampleQueries }, { alias: "name", - operator_codes: ["in, not_in"], - class: Array + operator_codes: ["in", "notin"], + class: String }, { alias: "age", From ea78a77e58ddadbce910b9b730d34647f1dffd37 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 23 Jul 2019 08:38:18 -0600 Subject: [PATCH 46/48] add filter method --- lib/query_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/query_helper.rb b/lib/query_helper.rb index cb3a662..e8413b0 100644 --- a/lib/query_helper.rb +++ b/lib/query_helper.rb @@ -58,6 +58,10 @@ def update_query(query: nil, model:nil, bind_variables: {}) @bind_variables.merge!(bind_variables) end + def add_filter(operator_code:, criterion:, comparate:) + @sql_filter.filter_values["comparate"] = { operator_code => criterion } + end + def execute_query # Correctly set the query and model based on query type determine_query_type() From d568831991c09c69a45260ee44b75899a04f1902 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 23 Jul 2019 08:44:05 -0600 Subject: [PATCH 47/48] version update --- lib/query_helper/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/query_helper/version.rb b/lib/query_helper/version.rb index fb29c53..336a062 100644 --- a/lib/query_helper/version.rb +++ b/lib/query_helper/version.rb @@ -1,3 +1,3 @@ class QueryHelper - VERSION = "0.2.10" + VERSION = "0.0.0" end From d7c4d93afc23c54702013969d7f4ee6044178063 Mon Sep 17 00:00:00 2001 From: Evan McDaniel Date: Tue, 23 Jul 2019 08:44:14 -0600 Subject: [PATCH 48/48] remove old specs --- Gemfile.lock | 2 +- spec/query_helper/associations_spec.rb | 41 ---- spec/query_helper/column_map_spec.rb | 26 --- spec/query_helper/filter_spec.rb | 175 ------------------ spec/query_helper/query_string_spec.rb | 33 ---- spec/query_helper/sql_spec.rb | 49 ----- spec/query_helper_spec.rb | 15 -- ...ller_test.rb => rails_integration_spec.rb} | 0 8 files changed, 1 insertion(+), 340 deletions(-) delete mode 100644 spec/query_helper/associations_spec.rb delete mode 100644 spec/query_helper/column_map_spec.rb delete mode 100644 spec/query_helper/filter_spec.rb delete mode 100644 spec/query_helper/query_string_spec.rb delete mode 100644 spec/query_helper/sql_spec.rb delete mode 100644 spec/query_helper_spec.rb rename spec/{controller_test.rb => rails_integration_spec.rb} (100%) diff --git a/Gemfile.lock b/Gemfile.lock index 2e5cc72..55559d8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - query_helper (0.2.10) + query_helper (0.0.0) activerecord (~> 5.0) activesupport (~> 5.0) diff --git a/spec/query_helper/associations_spec.rb b/spec/query_helper/associations_spec.rb deleted file mode 100644 index d1aae4d..0000000 --- a/spec/query_helper/associations_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -require "spec_helper" - -RSpec.describe QueryHelper::Associations do - - describe "process_association_params" do - it "parses association params" do - associations = QueryHelper::Associations.process_association_params("parent") - expect(associations).to eq([:parent]) - end - end - - describe "load_associations" do - it "loads associations" do - associations = QueryHelper::Associations.process_association_params("parent") - payload = Child.all - results = QueryHelper::Associations.load_associations(payload: payload, associations: associations) - results.each do |child| - expect(child["parent_id"]).to eq(child["parent"]["id"]) - end - end - end - - describe 'json_associations' do - subject { described_class.json_associations(associations) } - - context 'nested associations' do - let(:associations) do - [:parent, - children: [:grand_children], - pets: :grand_pets, - messages: { author: [:avatar, :profile] }] - end - - it 'translates to as_json format' do - expect(subject).to eq([:parent, children: { include: [:grand_children] }, - pets: { include: [:grand_pets] }, - messages: { include: [ author: { include: [:avatar, :profile] }]}]) - end - end - end -end diff --git a/spec/query_helper/column_map_spec.rb b/spec/query_helper/column_map_spec.rb deleted file mode 100644 index 7a05a48..0000000 --- a/spec/query_helper/column_map_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -require "spec_helper" - -RSpec.describe QueryHelper::ColumnMap do - let(:valid_operator_codes) {["gte", "lte", "gt", "lt", "eql", "noteql", "in", "notin", "null"]} - - describe ".create_from_hash" do - let(:hash) do - { - "column1" => "table.column1", - "column2" => "table.column2", - "column3" => {sql_expression: "sum(table.column3)", aggregate: true}, - "column4" => {sql_expression: "sum(table.column4)", aggregate: true}, - } - end - - it "creates an array of column maps" do - map = described_class.create_from_hash(hash) - expect(map.length).to eq(hash.keys.length) - expect(map.first.alias_name).to eq(hash.keys.first) - expect(map.first.sql_expression).to eq(hash.values.first) - expect(map.last.alias_name).to eq(hash.keys.last) - expect(map.last.sql_expression).to eq(hash.values.last[:sql_expression]) - expect(map.last.aggregate).to be true - end - end -end diff --git a/spec/query_helper/filter_spec.rb b/spec/query_helper/filter_spec.rb deleted file mode 100644 index ba67b4e..0000000 --- a/spec/query_helper/filter_spec.rb +++ /dev/null @@ -1,175 +0,0 @@ -require "spec_helper" - -RSpec.describe QueryHelper::Filter do - let(:valid_operator_codes) {["gte", "lte", "gt", "lt", "eql", "noteql", "in", "notin", "null"]} - - describe ".sql_string" do - let(:filter) do - described_class.new( - operator_code: "gte", - criterion: Time.now, - comparate: "children.age" - ) - end - - it "creates sql string" do - sql_string = filter.sql_string() - expect(sql_string).to eq("#{filter.comparate} #{filter.operator} :#{filter.bind_variable}") - end - - it "creates array correctly for in/not in" - it "lowercases text correctly" - it "creates sql_string correctly for null/not null comparisions" - end - - describe ".translate_operator_code" do - - # TODO: fix - Fails because criterion fails depending on the operator - # context "valid operator codes" do - # it "translates operator code correctly" do - # valid_operator_codes.each do |code| - # filter = described_class.new( - # operator_code: code, - # criterion: Faker::Number.between(0, 100), - # comparate: "children.age" - # ) - # expect(filter.operator).to_not be_nil - # end - # end - # end - - context "invalid operator code" do - it "raises an ArugmentError" do - expect{ - described_class.new( - operator_code: "fake_code", - criterion: Faker::Number.between(0, 100), - comparate: "children.age" - ) - }.to raise_error(InvalidQueryError) - end - end - end - - describe ".validate_criterion" do - RSpec.shared_examples "validates criterion" do - it "validates criterion" do - expect(filter.send(:validate_criterion)).to be true - end - end - - RSpec.shared_examples "invalidates criterion" do - it "validates criterion" do - expect{filter.send(:validate_criterion)}.to raise_error(InvalidQueryError) - end - end - - context "valid numeric criterion (gte, lte, gt, lt)" do - include_examples "validates criterion" - - let(:filter) do - described_class.new( - operator_code: "gte", - criterion: Faker::Number.between(0, 100), - comparate: "children.age" - ) - end - end - - context "valid date criterion (gte, lte, gt, lt)" do - include_examples "validates criterion" - - let(:filter) do - described_class.new( - operator_code: "gte", - criterion: Date.today, - comparate: "children.age" - ) - end - end - - context "valid time criterion (gte, lte, gt, lt)" do - include_examples "validates criterion" - - let(:filter) do - described_class.new( - operator_code: "gte", - criterion: Time.now, - comparate: "children.age" - ) - end - end - - context "invalid criterion (gte, lte, gt, lt)" do - include_examples "invalidates criterion" - - let(:filter) do - described_class.new( - operator_code: "gte", - criterion: "hello", - comparate: "children.age" - ) - end - end - - context "valid array criterion (in, notin)" do - include_examples "validates criterion" - - let(:filter) do - described_class.new( - operator_code: "in", - criterion: [1,2,3,4], - comparate: "children.age" - ) - end - end - - context "invalid criterion (in, notin)" do - include_examples "invalidates criterion" - - let(:filter) do - described_class.new( - operator_code: "in", - criterion: Date.today, - comparate: "children.age" - ) - end - end - - context "valid 'true' boolean criterion (null)" do - include_examples "validates criterion" - - let(:filter) do - described_class.new( - operator_code: "null", - criterion: true, - comparate: "children.age" - ) - end - end - - context "valid 'false' boolean criterion (null)" do - include_examples "validates criterion" - - let(:filter) do - described_class.new( - operator_code: "null", - criterion: false, - comparate: "children.age" - ) - end - end - - context "invalid boolean (null)" do - include_examples "invalidates criterion" - - let(:filter) do - described_class.new( - operator_code: "null", - criterion: "stringything", - comparate: "children.age" - ) - end - end - end -end diff --git a/spec/query_helper/query_string_spec.rb b/spec/query_helper/query_string_spec.rb deleted file mode 100644 index 2f2cae0..0000000 --- a/spec/query_helper/query_string_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require "spec_helper" - -RSpec.describe QueryHelper::QueryString do - let(:complex_query) do - query = %{ - with cte as ( - select * from table1 - ), cte1 as ( - select column1, column2 from table2 - ) - select a, b, c, d, sum(e) - from table1 - join cte on cte.a = table1.a - where string = string - group by a,b,c,d - having sum(e) > 1 - order by random_column - } - described_class.new(query) - end - - let(:simple_query) do - query = "select a from b" - described_class.new(query) - end - - let(:simple_group_by_query) do - query = "select sum(a) from b group by c" - described_class.new(query) - end - - it "pending tests" -end diff --git a/spec/query_helper/sql_spec.rb b/spec/query_helper/sql_spec.rb deleted file mode 100644 index 47eaaeb..0000000 --- a/spec/query_helper/sql_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require "spec_helper" - -RSpec.describe QueryHelper::Sql do - let(:query) do - %{ - select parents.id, parents.name, parents.age, count(children.id) as children_count - from parents - join children on parents.id = children.parent_id - group by parents.id - } - end - - let(:column_mappings) do - { - "children_count" => {sql_expression: "count(children.id)", aggregate: true}, - # "name" => "parents.name", - # "age" => "parents.age" - } - end - - let(:filters) { {"age"=>{"lt"=>100}, "children_count"=>{"gt"=>0}} } - - let(:sorts) {"name:asc:lowercase,age:desc"} - - let(:includes) {"children"} - - let(:as_json_options) {{ methods: [:favorite_star_wars_character] }} - - it "returns a payload" do - sql_query = described_class.new( - model: Parent, - query: query, - sorts: sorts, - column_mappings: column_mappings, - filters: filters, - associations: includes, - as_json_options: as_json_options, - page: 1, - per_page: 5, - run: true - ) - results = sql_query.payload() - expected_results = Parent.all.to_a.select{ |p| p.children.length > 1 && p.age < 100 } - expect(results[:pagination][:count]).to eq(expected_results.length) - expect(results[:pagination][:per_page]).to eq(5) - expect(results[:pagination][:current_page]).to eq(1) - expect(results[:data].length).to eq(5) - end -end diff --git a/spec/query_helper_spec.rb b/spec/query_helper_spec.rb deleted file mode 100644 index 410229f..0000000 --- a/spec/query_helper_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "spec_helper" - -# RSpec.describe QueryHelper do -# -# it "text" do -# byebug -# end -# end - -# RSpec.describe 'Requests', type: :request do -# it "text" do -# get '/parents' -# byebug -# end -# end diff --git a/spec/controller_test.rb b/spec/rails_integration_spec.rb similarity index 100% rename from spec/controller_test.rb rename to spec/rails_integration_spec.rb