simple-sql 0.5.36 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +6 -1
- data/.ruby-version +1 -1
- data/Gemfile +5 -1
- data/Makefile +4 -12
- data/README.md +188 -86
- data/VERSION +1 -1
- data/bin/db_restore +7 -1
- data/bin/pg +7 -1
- data/config/database.yml +2 -2
- data/lib/simple/sql/config.rb +7 -2
- data/lib/simple/sql/connection/base.rb +1 -0
- data/lib/simple/sql/connection/insert.rb +3 -2
- data/lib/simple/sql/connection/reflection.rb +2 -0
- data/lib/simple/sql/connection/scope/count_by_groups.rb +91 -33
- data/lib/simple/sql/connection/scope/search.rb +2 -1
- data/lib/simple/sql/connection/scope/where.rb +3 -3
- data/lib/simple/sql/connection.rb +1 -0
- data/lib/simple/sql/helpers/decoder.rb +10 -2
- data/lib/simple/sql/helpers/row_converter.rb +3 -3
- data/lib/simple/sql/monkey_patches.rb +4 -0
- data/lib/simple/sql/result/association_loader.rb +3 -3
- data/lib/simple/sql/result.rb +1 -0
- data/lib/simple/sql/version.rb +2 -0
- data/scripts/integration_tests +49 -0
- data/simple-sql.gemspec +6 -16
- data/spec/simple/sql/config_spec.rb +1 -1
- data/spec/simple/sql/count_by_groups_spec.rb +37 -18
- data/spec/simple/sql/insert_spec.rb +2 -2
- data/spec/simple/sql/scope_spec.rb +8 -8
- data/spec/simple/sql/version_spec.rb +1 -1
- data/spec/support/001_database.rb +11 -2
- metadata +15 -84
@@ -19,55 +19,113 @@ class Simple::SQL::Connection::Scope
|
|
19
19
|
#
|
20
20
|
def enumerate_groups(sql_fragment)
|
21
21
|
sql = order_by(nil).to_sql(pagination: false)
|
22
|
+
@connection.all "SELECT DISTINCT #{sql_fragment} FROM (#{sql}) sq", *args
|
23
|
+
end
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
# cost estimates are good, but are hard to check against a hard coded value.
|
26
|
-
# see https://issues.mediafellows.com/issues/75232
|
27
|
-
#
|
28
|
-
# if cost > 10_000
|
29
|
-
# raise "enumerate_groups(#{sql_fragment.inspect}) takes too much time. Make sure to create a suitable index"
|
30
|
-
# end
|
25
|
+
def count_by(sql_fragment)
|
26
|
+
expect! sql_fragment => String
|
31
27
|
|
32
|
-
|
33
|
-
var_name = "$#{@args.count + 1}"
|
34
|
-
cur = @connection.ask "SELECT MIN(#{sql_fragment}) FROM (#{sql}) sq", *args
|
28
|
+
sql = order_by(nil).to_sql(pagination: false)
|
35
29
|
|
36
|
-
|
37
|
-
|
38
|
-
|
30
|
+
recs = @connection.all "SELECT COUNT(*) AS count, #{sql_fragment} AS group FROM (#{sql}) sq GROUP BY #{sql_fragment}", *args
|
31
|
+
|
32
|
+
# if we count by a single value (e.g. `count_by("role_id")`) each entry in recs consists of an array [group_value, count].
|
33
|
+
# The resulting Hash will have entries of group_value => count.
|
34
|
+
if recs.first&.length == 2
|
35
|
+
recs.each_with_object({}) do |count_and_group, hsh|
|
36
|
+
count, group = *count_and_group
|
37
|
+
hsh[group] = count
|
38
|
+
end
|
39
|
+
else
|
40
|
+
recs.each_with_object({}) do |count_and_group, hsh|
|
41
|
+
count, *group = *count_and_group
|
42
|
+
hsh[group] = count
|
43
|
+
end
|
39
44
|
end
|
40
|
-
|
41
|
-
groups
|
42
45
|
end
|
43
46
|
|
44
|
-
|
45
|
-
sql = order_by(nil).to_sql(pagination: false)
|
47
|
+
private
|
46
48
|
|
47
|
-
|
48
|
-
|
49
|
+
# cost estimate threshold for count_by method. Can be set to false, true, or
|
50
|
+
# a number.
|
51
|
+
#
|
52
|
+
# Note that cost estimates are problematic, since they are not reported in
|
53
|
+
# any "real" unit, meaning any comparison really is a bit pointless.
|
54
|
+
COUNT_BY_ESTIMATE_COST_THRESHOLD = 10_000
|
55
|
+
|
56
|
+
# estimates the cost to run a sql query. If COUNT_BY_ESTIMATE_COST_THRESHOLD
|
57
|
+
# is set and the cost estimate is less than COUNT_BY_ESTIMATE_COST_THRESHOLD
|
58
|
+
# \a count_by_estimate is using the estimating code path.
|
59
|
+
def use_count_by_estimate?(sql_group_by_fragment)
|
60
|
+
case COUNT_BY_ESTIMATE_COST_THRESHOLD
|
61
|
+
when true then true
|
62
|
+
when false then false
|
63
|
+
else
|
64
|
+
# estimate the effort to exact counting over all groups.
|
65
|
+
base_sql = order_by(nil).to_sql(pagination: false)
|
66
|
+
count_sql = "SELECT COUNT(*) FROM (#{base_sql}) sq GROUP BY #{sql_group_by_fragment}"
|
67
|
+
cost = @connection.estimate_cost count_sql, *args
|
68
|
+
|
69
|
+
cost >= COUNT_BY_ESTIMATE_COST_THRESHOLD
|
70
|
+
end
|
49
71
|
end
|
50
72
|
|
73
|
+
public
|
74
|
+
|
51
75
|
def count_by_estimate(sql_fragment)
|
52
|
-
|
76
|
+
expect! sql_fragment => String
|
53
77
|
|
54
|
-
|
55
|
-
# disabled (see https://issues.mediafellows.com/issues/75237).
|
78
|
+
return count_by(sql_fragment) unless use_count_by_estimate?(sql_fragment)
|
56
79
|
|
57
|
-
|
58
|
-
|
80
|
+
# iterate over all groups, estimating the count for each.
|
81
|
+
#
|
82
|
+
# For larger groups we'll use that estimate - preventing a full table scan.
|
83
|
+
# Groups smaller than EXACT_COUNT_THRESHOLD are counted exactly - in the
|
84
|
+
# hope that this query can be answered from an index.
|
85
|
+
|
86
|
+
#
|
87
|
+
# Usually Simple::SQL.all normalizes each result row into its first value,
|
88
|
+
# if the row only consists of a single value. Here, however, we don't
|
89
|
+
# know the width of a group; so to understand this we just add a dummy
|
90
|
+
# value to the sql_fragment and then remove it again.
|
91
|
+
#
|
92
|
+
groups = enumerate_groups("1 AS __dummy__, #{sql_fragment}")
|
93
|
+
groups = groups.each(&:shift)
|
59
94
|
|
60
|
-
|
95
|
+
# no groups? well, then...
|
96
|
+
return {} if groups.empty?
|
61
97
|
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
65
|
-
|
98
|
+
#
|
99
|
+
# The estimating code only works for groups of size 1. This is a limitation
|
100
|
+
# of simple-sql - for larger groups we would have to be able to encode arrays
|
101
|
+
# of arrays on their way to the postgres server. We are not able to do that
|
102
|
+
# currently.
|
103
|
+
#
|
104
|
+
group_size = groups.first&.length
|
105
|
+
if group_size > 1
|
106
|
+
return count_by(sql_fragment)
|
107
|
+
end
|
108
|
+
|
109
|
+
# The code below only works for groups of size 1
|
110
|
+
groups = groups.map(&:first)
|
111
|
+
|
112
|
+
#
|
113
|
+
# Now we estimate the count of entries in each group. For large groups we
|
114
|
+
# just use the estimate - because it is usually pretty close to being correct.
|
115
|
+
# Small groups are collected in the `sparse_groups` array, to be counted
|
116
|
+
# exactly later on.
|
117
|
+
#
|
66
118
|
|
67
119
|
counts = {}
|
120
|
+
|
68
121
|
sparse_groups = []
|
69
|
-
|
70
|
-
|
122
|
+
base_sql = order_by(nil).to_sql(pagination: false)
|
123
|
+
|
124
|
+
var_name = "$#{@args.count + 1}"
|
125
|
+
|
126
|
+
groups.each do |group|
|
127
|
+
scope = @connection.scope("SELECT * FROM (#{base_sql}) sq WHERE #{sql_fragment}=#{var_name}", args + [group])
|
128
|
+
|
71
129
|
estimated_count = scope.send(:estimated_count)
|
72
130
|
counts[group] = estimated_count
|
73
131
|
sparse_groups << group if estimated_count < EXACT_COUNT_THRESHOLD
|
@@ -77,7 +135,7 @@ class Simple::SQL::Connection::Scope
|
|
77
135
|
unless sparse_groups.empty?
|
78
136
|
sparse_counts = @connection.all <<~SQL, *args, sparse_groups
|
79
137
|
SELECT #{sql_fragment} AS group, COUNT(*) AS count
|
80
|
-
FROM (#{
|
138
|
+
FROM (#{base_sql}) sq
|
81
139
|
WHERE #{sql_fragment} = ANY(#{var_name})
|
82
140
|
GROUP BY #{sql_fragment}
|
83
141
|
SQL
|
@@ -57,7 +57,7 @@ module Simple::SQL::Connection::Scope::Search
|
|
57
57
|
return scope if filters.empty?
|
58
58
|
|
59
59
|
filters.inject(scope) do |scp, (k, v)|
|
60
|
-
scp.where k => resolve_static_matches(v, column_type: column_types.fetch(k))
|
60
|
+
scp.where({ k => resolve_static_matches(v, column_type: column_types.fetch(k)) })
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
@@ -78,6 +78,7 @@ module Simple::SQL::Connection::Scope::Search
|
|
78
78
|
def empty_filter?(_key, value)
|
79
79
|
return true if value.nil?
|
80
80
|
return true if value.is_a?(Enumerable) && value.empty? # i.e. Hash, Array
|
81
|
+
|
81
82
|
false
|
82
83
|
end
|
83
84
|
|
@@ -23,11 +23,9 @@ class Simple::SQL::Connection::Scope
|
|
23
23
|
# scope = scope.where(metadata: { uid: 1 }, jsonb: false)
|
24
24
|
#
|
25
25
|
def where(sql_fragment, arg = :__dummy__no__arg, placeholder: "?", jsonb: true)
|
26
|
-
duplicate.
|
26
|
+
duplicate.where!(sql_fragment, arg, placeholder: placeholder, jsonb: jsonb)
|
27
27
|
end
|
28
28
|
|
29
|
-
private
|
30
|
-
|
31
29
|
def where!(first_arg, arg = :__dummy__no__arg, placeholder: "?", jsonb: true)
|
32
30
|
if arg != :__dummy__no__arg
|
33
31
|
where_sql_with_argument!(first_arg, arg, placeholder: placeholder)
|
@@ -40,6 +38,8 @@ class Simple::SQL::Connection::Scope
|
|
40
38
|
self
|
41
39
|
end
|
42
40
|
|
41
|
+
private
|
42
|
+
|
43
43
|
def where_sql!(sql_fragment)
|
44
44
|
@where << sql_fragment
|
45
45
|
end
|
@@ -7,6 +7,7 @@ module Simple::SQL::Helpers::Decoder
|
|
7
7
|
# rubocop:disable Metrics/AbcSize
|
8
8
|
# rubocop:disable Metrics/CyclomaticComplexity
|
9
9
|
# rubocop:disable Naming/UncommunicativeMethodParamName
|
10
|
+
# rubocop:disable Style/MultipleComparison
|
10
11
|
def decode_value(type, s)
|
11
12
|
case type
|
12
13
|
when :unknown then s
|
@@ -18,8 +19,8 @@ module Simple::SQL::Helpers::Decoder
|
|
18
19
|
when :'integer[]' then s.scan(/-?\d+/).map { |part| Integer(part) }
|
19
20
|
when :"character varying[]" then parse_pg_array(s)
|
20
21
|
when :"text[]" then parse_pg_array(s)
|
21
|
-
when :"timestamp without time zone" then
|
22
|
-
when :"timestamp with time zone" then
|
22
|
+
when :"timestamp without time zone" then decode_time(s)
|
23
|
+
when :"timestamp with time zone" then decode_time(s)
|
23
24
|
when :hstore then HStore.parse(s)
|
24
25
|
when :json then ::JSON.parse(s)
|
25
26
|
when :jsonb then ::JSON.parse(s)
|
@@ -36,6 +37,12 @@ module Simple::SQL::Helpers::Decoder
|
|
36
37
|
require "pg_array_parser"
|
37
38
|
extend PgArrayParser
|
38
39
|
|
40
|
+
def decode_time(s)
|
41
|
+
return s if s.is_a?(Time)
|
42
|
+
|
43
|
+
::Time.parse(s)
|
44
|
+
end
|
45
|
+
|
39
46
|
# HStore parsing
|
40
47
|
module HStore
|
41
48
|
extend self
|
@@ -68,6 +75,7 @@ end
|
|
68
75
|
|
69
76
|
module Simple::SQL::Helpers::Decoder
|
70
77
|
def self.new(result, into:, column_info:)
|
78
|
+
# rubocop:disable Lint/ElseLayout
|
71
79
|
if into == Hash then HashRecord.new(column_info)
|
72
80
|
elsif result.nfields == 1 then SingleColumn.new(column_info)
|
73
81
|
else MultiColumns.new(column_info)
|
@@ -27,7 +27,7 @@ module Simple::SQL::Helpers::RowConverter
|
|
27
27
|
ary.first
|
28
28
|
end
|
29
29
|
|
30
|
-
class TypeConverter
|
30
|
+
class TypeConverter # :nodoc:
|
31
31
|
def initialize(type:, associations:)
|
32
32
|
@type = type
|
33
33
|
@associations = associations
|
@@ -57,7 +57,7 @@ module Simple::SQL::Helpers::RowConverter
|
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
|
-
class ImmutableConverter < TypeConverter
|
60
|
+
class ImmutableConverter < TypeConverter # :nodoc:
|
61
61
|
Immutable = ::Simple::Immutable
|
62
62
|
|
63
63
|
def build_row_in_target_type(hsh)
|
@@ -65,7 +65,7 @@ module Simple::SQL::Helpers::RowConverter
|
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
68
|
-
class TypeConverter2 < TypeConverter
|
68
|
+
class TypeConverter2 < TypeConverter # :nodoc:
|
69
69
|
def initialize(type:, associations:, fq_table_name:)
|
70
70
|
super(type: type, associations: associations)
|
71
71
|
@fq_table_name = fq_table_name
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# This file contains some monkey patches
|
2
2
|
|
3
|
+
# rubocop:disable Lint/DuplicateMethods
|
4
|
+
|
3
5
|
module Simple::SQL::MonkeyPatches
|
4
6
|
def self.warn(msg)
|
5
7
|
return if ENV["SIMPLE_SQL_SILENCE"] == "1"
|
@@ -29,6 +31,7 @@ when /^5.2/
|
|
29
31
|
class ActiveRecord::ConnectionAdapters::ConnectionPool::Reaper
|
30
32
|
def run
|
31
33
|
return unless frequency && frequency > 0
|
34
|
+
|
32
35
|
Simple::SQL::MonkeyPatches.warn "simple-sql disables reapers for all connection pools, see https://github.com/rails/rails/issues/33600"
|
33
36
|
end
|
34
37
|
end
|
@@ -45,6 +48,7 @@ when /^6/
|
|
45
48
|
class ActiveRecord::ConnectionAdapters::ConnectionPool::Reaper
|
46
49
|
def run
|
47
50
|
return unless frequency && frequency > 0
|
51
|
+
|
48
52
|
Simple::SQL::MonkeyPatches.warn "simple-sql disables reapers for all connection pools, see https://github.com/rails/rails/issues/33600"
|
49
53
|
end
|
50
54
|
end
|
@@ -92,8 +92,8 @@ module ::Simple::SQL::Result::AssociationLoader # :nodoc:
|
|
92
92
|
|
93
93
|
foreign_ids = H.pluck(records, belonging_column).uniq.compact
|
94
94
|
|
95
|
-
scope = connection.scope(table: relation.having_table)
|
96
|
-
scope = scope.where(having_column => foreign_ids)
|
95
|
+
scope = connection.scope({ table: relation.having_table })
|
96
|
+
scope = scope.where({ having_column => foreign_ids })
|
97
97
|
|
98
98
|
recs = connection.all(scope, into: Hash)
|
99
99
|
recs_by_id = H.by_key(recs, having_column)
|
@@ -122,7 +122,7 @@ module ::Simple::SQL::Result::AssociationLoader # :nodoc:
|
|
122
122
|
host_ids = H.pluck(records, having_column).uniq.compact
|
123
123
|
|
124
124
|
scope = connection.scope(table: relation.belonging_table)
|
125
|
-
scope = scope.where(belonging_column => host_ids)
|
125
|
+
scope = scope.where({ belonging_column => host_ids })
|
126
126
|
scope = scope.order_by(order_by) if order_by
|
127
127
|
|
128
128
|
recs = connection.all(scope, into: Hash)
|
data/lib/simple/sql/result.rb
CHANGED
data/lib/simple/sql/version.rb
CHANGED
@@ -6,6 +6,7 @@ module Simple
|
|
6
6
|
def version(name)
|
7
7
|
spec = Gem.loaded_specs[name]
|
8
8
|
return "unreleased" unless spec
|
9
|
+
|
9
10
|
version = spec.version.to_s
|
10
11
|
version += "+unreleased" if unreleased?(spec)
|
11
12
|
version
|
@@ -17,6 +18,7 @@ module Simple
|
|
17
18
|
return false unless defined?(Bundler::Source::Gemspec)
|
18
19
|
return true if spec.source.is_a?(::Bundler::Source::Gemspec)
|
19
20
|
return true if spec.source.is_a?(::Bundler::Source::Path)
|
21
|
+
|
20
22
|
false
|
21
23
|
end
|
22
24
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -eu -o pipefail
|
4
|
+
|
5
|
+
echo "Starting integration tests. We log into log/integration_tests.log"
|
6
|
+
|
7
|
+
rm log/integration_tests.log
|
8
|
+
touch log/integration_tests.log
|
9
|
+
|
10
|
+
export SIMPLE_SQL_SILENCE=1
|
11
|
+
|
12
|
+
run_test() {
|
13
|
+
local activerecord_spec=$1
|
14
|
+
local pg_spec=$2
|
15
|
+
|
16
|
+
export SIMPLE_SQL_ACTIVERECORD_SPECS="$activerecord_spec"
|
17
|
+
export SIMPLE_SQL_PG_SPECS="$pg_spec"
|
18
|
+
|
19
|
+
printf "=== Running test w/SIMPLE_SQL_ACTIVERECORD_SPECS='%s' SIMPLE_SQL_PG_SPECS='%s'\n" "$SIMPLE_SQL_ACTIVERECORD_SPECS" "$SIMPLE_SQL_PG_SPECS" | tee -a log/integration_tests.log
|
20
|
+
|
21
|
+
if ! bundle update >> log/integration_tests.log ; then
|
22
|
+
echo "Bundling failed"
|
23
|
+
set -xv
|
24
|
+
bundle update
|
25
|
+
exit 1
|
26
|
+
fi
|
27
|
+
|
28
|
+
if ! bundle exec rspec >> log/integration_tests.log ; then
|
29
|
+
echo "Tests failed"
|
30
|
+
set -xv
|
31
|
+
bundle exec rspec
|
32
|
+
exit 1
|
33
|
+
fi
|
34
|
+
}
|
35
|
+
|
36
|
+
run_test "> 5,< 6" "~> 0.20"
|
37
|
+
run_test "> 5,< 6" "~> 1.0.0"
|
38
|
+
run_test "> 5,< 6" "~> 1.1.0"
|
39
|
+
run_test "> 5,< 6" "~> 1.2.0"
|
40
|
+
run_test "> 5,< 6" "~> 1.3.0"
|
41
|
+
|
42
|
+
run_test "> 6,< 7" "~> 1.1.0"
|
43
|
+
run_test "> 6,< 7" "~> 1.2.0"
|
44
|
+
run_test "> 6,< 7" "~> 1.3.0"
|
45
|
+
|
46
|
+
run_test "> 7,< 8" "~> 1.1.0"
|
47
|
+
run_test "> 7,< 8" "~> 1.2.0"
|
48
|
+
run_test "> 7,< 8" "~> 1.3.0"
|
49
|
+
|
data/simple-sql.gemspec
CHANGED
@@ -21,29 +21,19 @@ Gem::Specification.new do |gem|
|
|
21
21
|
# executables are used for development purposes only
|
22
22
|
gem.executables = []
|
23
23
|
|
24
|
-
gem.required_ruby_version = '~>
|
24
|
+
gem.required_ruby_version = '~> 3.3'
|
25
25
|
|
26
26
|
gem.add_dependency 'pg_array_parser', '~> 0', '>= 0.0.9'
|
27
|
-
gem.add_dependency 'pg', '~> 0.20'
|
28
27
|
gem.add_dependency 'expectation', '~> 1'
|
29
28
|
|
30
29
|
gem.add_dependency 'digest-crc', '~> 0'
|
31
30
|
gem.add_dependency 'simple-immutable', '~> 1.0'
|
32
31
|
|
32
|
+
pg_specs = ENV["SIMPLE_SQL_PG_SPECS"] || '~> 1.0'
|
33
|
+
gem.add_dependency 'pg', *(pg_specs.split(","))
|
34
|
+
|
33
35
|
# during tests we check the SIMPLE_SQL_ACTIVERECORD_SPECS environment setting.
|
34
36
|
# Run make tests to run all tests
|
35
|
-
|
36
|
-
|
37
|
-
else
|
38
|
-
gem.add_dependency 'activerecord', '>= 5.2.4.5', '< 6.1'
|
39
|
-
end
|
40
|
-
|
41
|
-
# optional gems (required by some of the parts)
|
42
|
-
|
43
|
-
# development gems
|
44
|
-
gem.add_development_dependency 'pg', '0.20'
|
45
|
-
gem.add_development_dependency 'rake', '>= 12.3.3'
|
46
|
-
gem.add_development_dependency 'rspec', '~> 3.7'
|
47
|
-
gem.add_development_dependency 'rubocop', '~> 0.61.1'
|
48
|
-
gem.add_development_dependency 'simplecov', '~> 0'
|
37
|
+
activerecord_specs = ENV["SIMPLE_SQL_ACTIVERECORD_SPECS"] || '< 6.1'
|
38
|
+
gem.add_dependency 'activerecord', '>= 5.2.4.5', *(activerecord_specs.split(","))
|
49
39
|
end
|
@@ -1,44 +1,63 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
3
|
describe "Simple::SQL::Connection::Scope#count_by" do
|
4
|
-
let!(:users)
|
5
|
-
let(:
|
6
|
-
|
4
|
+
let!(:users) { 1.upto(10).map { |i| create(:user, role_id: i) } }
|
5
|
+
let(:scope) { SQL.scope("SELECT * FROM users") }
|
6
|
+
|
7
|
+
let(:all_role_ids) { 1.upto(10).to_a }
|
8
|
+
let(:all_role_ids_w_squares) { all_role_ids.map { |role_id| [role_id, role_id*role_id] } }
|
9
|
+
|
10
|
+
before do
|
11
|
+
# initially we have 10 users, one per role_id in the range 1 .. 10
|
12
|
+
# This adds another 3 users with role_id of 1.
|
13
|
+
create(:user, role_id: 1)
|
14
|
+
create(:user, role_id: 1)
|
15
|
+
create(:user, role_id: 1)
|
16
|
+
end
|
7
17
|
|
8
18
|
describe "enumerate_groups" do
|
9
|
-
it "returns all groups" do
|
19
|
+
it "returns all groups by a single column" do
|
10
20
|
expect(scope.enumerate_groups("role_id")).to contain_exactly(*all_role_ids)
|
11
|
-
|
21
|
+
end
|
22
|
+
|
23
|
+
it "obeys where conditions" do
|
24
|
+
expect(scope.where("role_id < $1", 4).enumerate_groups("role_id")).to contain_exactly(1,2,3)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "counts all groups by multiple columns" do
|
28
|
+
expect(scope.where("role_id < $1", 4).enumerate_groups("role_id, role_id * role_id")).to contain_exactly([1, 1], [2, 4], [3, 9])
|
12
29
|
end
|
13
30
|
end
|
14
31
|
|
15
32
|
describe "count_by" do
|
16
|
-
it "counts all groups" do
|
17
|
-
create(:user, role_id: 1)
|
18
|
-
create(:user, role_id: 1)
|
19
|
-
create(:user, role_id: 1)
|
20
|
-
|
33
|
+
it "counts all groups by a single column" do
|
21
34
|
expect(scope.count_by("role_id")).to include(1 => 4)
|
22
35
|
expect(scope.count_by("role_id")).to include(2 => 1)
|
23
36
|
expect(scope.count_by("role_id").keys).to contain_exactly(*all_role_ids)
|
24
37
|
end
|
38
|
+
|
39
|
+
it "counts all groups by multiple columns" do
|
40
|
+
expect(scope.where("role_id < $1", 4).count_by("role_id, role_id * role_id")).to include([1,1] => 4)
|
41
|
+
expect(scope.where("role_id < $1", 4).count_by("role_id, role_id * role_id")).to include([2, 4] => 1)
|
42
|
+
expect(scope.where("role_id < $1", 4).count_by("role_id, role_id * role_id").keys).to contain_exactly([1, 1], [2, 4], [3, 9])
|
43
|
+
end
|
25
44
|
end
|
26
45
|
|
27
46
|
describe "count_by_estimate" do
|
28
47
|
before do
|
29
|
-
|
30
|
-
# but
|
31
|
-
allow(::Simple::SQL).to receive(:costs).and_return([0, 10_000])
|
48
|
+
expect_any_instance_of(Simple::SQL::Connection).to receive(:estimate_cost).at_least(:once).and_return(10_000)
|
32
49
|
end
|
33
|
-
|
34
|
-
it "counts all groups" do
|
35
|
-
create(:user, role_id: 1)
|
36
|
-
create(:user, role_id: 1)
|
37
|
-
create(:user, role_id: 1)
|
38
50
|
|
51
|
+
it "counts all groups by a single column" do
|
39
52
|
expect(scope.count_by_estimate("role_id")).to include(1 => 4)
|
40
53
|
expect(scope.count_by_estimate("role_id")).to include(2 => 1)
|
41
54
|
expect(scope.count_by_estimate("role_id").keys).to contain_exactly(*all_role_ids)
|
42
55
|
end
|
56
|
+
|
57
|
+
it "counts all groups by multiple columns and conditions" do
|
58
|
+
expect(scope.where("role_id < $1", 4).count_by_estimate("role_id, role_id * role_id")).to include([1,1] => 4)
|
59
|
+
expect(scope.where("role_id < $1", 4).count_by_estimate("role_id, role_id * role_id")).to include([2, 4] => 1)
|
60
|
+
expect(scope.where("role_id < $1", 4).count_by_estimate("role_id, role_id * role_id").keys).to contain_exactly([1, 1], [2, 4], [3, 9])
|
61
|
+
end
|
43
62
|
end
|
44
63
|
end
|
@@ -7,7 +7,7 @@ describe "Simple::SQL.insert" do
|
|
7
7
|
let!(:initial_ids) { SQL.all("SELECT id FROM users") }
|
8
8
|
|
9
9
|
it "inserts a single user" do
|
10
|
-
id = SQL.insert :users, first_name: "foo", last_name: "bar"
|
10
|
+
id = SQL.insert :users, { first_name: "foo", last_name: "bar" }
|
11
11
|
expect(id).to be_a(Integer)
|
12
12
|
expect(initial_ids).not_to include(id)
|
13
13
|
expect(SQL.ask("SELECT count(*) FROM users")).to eq(USER_COUNT+1)
|
@@ -19,7 +19,7 @@ describe "Simple::SQL.insert" do
|
|
19
19
|
end
|
20
20
|
|
21
21
|
it "returns the id" do
|
22
|
-
id = SQL.insert :users, first_name: "foo", last_name: "bar"
|
22
|
+
id = SQL.insert :users, { first_name: "foo", last_name: "bar" }
|
23
23
|
expect(id).to be_a(Integer)
|
24
24
|
expect(initial_ids).not_to include(id)
|
25
25
|
end
|
@@ -33,36 +33,36 @@ describe "Simple::SQL::Connection::Scope" do
|
|
33
33
|
|
34
34
|
context "that do not match" do
|
35
35
|
it "does not match with string keys" do
|
36
|
-
expect(SQL.ask(scope.where(id: -1))).to be_nil
|
36
|
+
expect(SQL.ask(scope.where({id: -1}))).to be_nil
|
37
37
|
end
|
38
38
|
|
39
39
|
it "does not match with symbol keys" do
|
40
|
-
expect(SQL.ask(scope.where("id" => -1))).to be_nil
|
40
|
+
expect(SQL.ask(scope.where({"id" => -1}))).to be_nil
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
44
|
context "that match" do
|
45
45
|
it "matches with string keys" do
|
46
|
-
expect(SQL.ask(scope.where("id" => user_id))).to eq(1)
|
46
|
+
expect(SQL.ask(scope.where({"id" => user_id}))).to eq(1)
|
47
47
|
end
|
48
48
|
|
49
49
|
it "matches with symbol keys" do
|
50
|
-
expect(SQL.ask(scope.where(id: user_id))).to eq(1)
|
50
|
+
expect(SQL.ask(scope.where({id: user_id}))).to eq(1)
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
54
|
context "with array arguments" do
|
55
55
|
it "matches against array arguments" do
|
56
|
-
expect(SQL.ask(scope.where("id" => [-333, user_id]))).to eq(1)
|
57
|
-
expect(SQL.ask(scope.where("id" => [-333, -1]))).to be_nil
|
58
|
-
expect(SQL.ask(scope.where("id" => []))).to be_nil
|
56
|
+
expect(SQL.ask(scope.where({"id" => [-333, user_id]}))).to eq(1)
|
57
|
+
expect(SQL.ask(scope.where({"id" => [-333, -1]}))).to be_nil
|
58
|
+
expect(SQL.ask(scope.where({"id" => []}))).to be_nil
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
62
62
|
context "with invalid arguments" do
|
63
63
|
it "raises an ArgumentError" do
|
64
64
|
expect {
|
65
|
-
scope.where(1 => 3)
|
65
|
+
scope.where({1 => 3})
|
66
66
|
}.to raise_error(ArgumentError)
|
67
67
|
end
|
68
68
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require "logger"
|
2
|
+
|
1
3
|
# connect to the database and setup the schema
|
2
4
|
require "active_record"
|
3
5
|
|
@@ -5,14 +7,21 @@ require "active_record"
|
|
5
7
|
require "simple-sql"
|
6
8
|
|
7
9
|
require "yaml"
|
8
|
-
|
10
|
+
|
11
|
+
path = "config/database.yml"
|
12
|
+
abc = if Psych::VERSION > '4.0'
|
13
|
+
YAML.safe_load(File.read(path), aliases: true)
|
14
|
+
else
|
15
|
+
YAML.safe_load(File.read(path), [], [], true)
|
16
|
+
end
|
17
|
+
|
9
18
|
ActiveRecord::Base.establish_connection(abc["test"])
|
10
19
|
|
11
20
|
if ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks=)
|
12
21
|
ActiveRecord::Base.raise_in_transactional_callbacks = true
|
13
22
|
end
|
14
23
|
|
15
|
-
ActiveRecord::Base.logger = Logger.new("log/test.log")
|
24
|
+
ActiveRecord::Base.logger = ::Logger.new("log/test.log")
|
16
25
|
|
17
26
|
ActiveRecord::Schema.define do
|
18
27
|
self.verbose = false
|