sparkql 1.2.8 → 1.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d6df2039c1c9c690fe60002b8374ec300e1ffab8496b02e668da3f77999d2e8
4
- data.tar.gz: a529df747695e8c43a45bec3f166cc8e1b2cfa34a671173a998b4620fe522a67
3
+ metadata.gz: 0f8f460c07a1a833cd0a80040f686b12a9579db3bc0ae60d1cc7aa1c73fe644d
4
+ data.tar.gz: 66a6701ad74dc5454d9e4dd2a14c0ea133715e10ba5babb676c90aed62e917bd
5
5
  SHA512:
6
- metadata.gz: e4a0fa65b7cb2b41198f239a6c68613939e3f6f0ce4ce20a9ae00b1ea4f7ae377cdd41b3b67f4a2da158344e33fa2d12d0a9cec7cc7c64a85a14e9f35bdffbdc
7
- data.tar.gz: 746cfed6761f6bdc8ec7dfe91ad45b20d44fd6a67a7fe13e184406a8fab533c1a4122435242447da803160c4c4e3f05a8a8e61ba3a83ca568f2dd4d2b305ed1a
6
+ metadata.gz: 661e847ae1aa0f336706d94129c0afdc7d0ba040c0c8f62b7a6e9248dcc990d8f0179ef153355e050bae2b8a2f676c7e358081280418986c6c0bbf27393ac572
7
+ data.tar.gz: 4b64da6af2453236ec0b9c0e240238241609c79e0d4b8cca2c126ecc9b5e3b7e9c585836cc0dceb9a5136cb9decb993bffdef0145720de8dd734b2b2f291f340
@@ -0,0 +1,51 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ release:
8
+ types: published
9
+
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - uses: actions/checkout@v3
17
+ - uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: '2.7'
20
+ bundler-cache: true
21
+
22
+ - run: |
23
+ gem install bundler -v 2.2.31
24
+ ./script/ci_build
25
+
26
+ publish:
27
+ runs-on: ubuntu-latest
28
+ env:
29
+ RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
30
+
31
+ # only run if this is a release event
32
+ if: github.event_name == 'release'
33
+
34
+ # require that the build job passed
35
+ needs: build
36
+
37
+ steps:
38
+ - uses: actions/checkout@v3
39
+ - uses: ruby/setup-ruby@v1
40
+ with:
41
+ ruby-version: '2.7'
42
+
43
+ - run: |
44
+ ./script/bootstrap
45
+ bundle exec rake build
46
+
47
+ mkdir -p $HOME/.gem
48
+ touch $HOME/.gem/credentials
49
+ printf -- "---\n:rubygems_api_key: ${RUBYGEMS_API_KEY}\n" > $HOME/.gem/credentials
50
+ chmod 0600 $HOME/.gem/credentials
51
+ gem push pkg/*.gem
data/.rubocop.yml ADDED
@@ -0,0 +1,111 @@
1
+ AllCops:
2
+ NewCops: disable
3
+ Exclude:
4
+ - 'bin/*'
5
+ - 'config/**/*'
6
+ - 'Rakefile'
7
+ - 'Capfile'
8
+ - 'Gemfile'
9
+ - 'Guardfile'
10
+ - 'test/factories/*'
11
+ - 'test/support/*'
12
+ - 'config/routes.rb'
13
+ - 'script/*'
14
+ - 'db/**/*'
15
+ - 'vendor/**/*'
16
+
17
+ Style/FrozenStringLiteralComment:
18
+ Enabled: false
19
+
20
+ Style/Documentation:
21
+ Enabled: false
22
+
23
+ Metrics/AbcSize:
24
+ Enabled: false
25
+
26
+ Metrics/BlockLength:
27
+ Enabled: false
28
+
29
+ Metrics/ClassLength:
30
+ Enabled: false
31
+
32
+ Metrics/CyclomaticComplexity:
33
+ Enabled: false
34
+
35
+ Layout/LineLength:
36
+ Enabled: false
37
+
38
+ Metrics/MethodLength:
39
+ Enabled: false
40
+
41
+ Metrics/ModuleLength:
42
+ Enabled: false
43
+
44
+ Metrics/PerceivedComplexity:
45
+ Enabled: false
46
+
47
+ # "Favor `unless` over `if` for negative conditions."
48
+ Style/NegatedIf:
49
+ Enabled: false
50
+ # safe_yaml seems to break all the things.
51
+ Security/YAMLLoad:
52
+ Enabled: false
53
+
54
+ # "Use a guard clause (`return unless extra_types.any?`) instead
55
+ # of wrapping the code inside a conditional expression."
56
+ #
57
+ # Justification: guard clauses don't work very well with long lines.
58
+ # Also, when there's an if check that (say) adds an error to a model
59
+ # validation, it makes more sense to wrap the operation in an if block
60
+ # than to guard the error entry with a double negative.
61
+ Style/GuardClause:
62
+ Enabled: false
63
+
64
+ # Justification:
65
+ #
66
+ # `class MyModule::ClassName` is a lot more concise, especially for tests
67
+ # covering a class that is within a module, than having to wrap the whole
68
+ # class in a module, and indent.
69
+ #
70
+ # "Use nested module/class definitions instead of compact style."
71
+ Style/ClassAndModuleChildren:
72
+ Enabled: false
73
+
74
+ # Justification:
75
+ #
76
+ # A single-line guard clause isn't always a good thing.
77
+ Style/IfUnlessModifier:
78
+ Enabled: false
79
+
80
+ # Justification:
81
+ #
82
+ # Hundreds of existing infractions, and it's not really that confusion to
83
+ # see regex without parens around it.
84
+ Lint/AmbiguousRegexpLiteral:
85
+ Enabled: false
86
+
87
+ # Justification:
88
+ #
89
+ # Is it so wrong to have a variable named fgo_listing_1, instead
90
+ # of fgo_listing1?
91
+ Naming/VariableNumber:
92
+ Enabled: false
93
+
94
+ # Justification:
95
+ #
96
+ # Explicit else's are much clearer than
97
+ # a branch that ends with an `elsif`, and presumes
98
+ # a nil else.
99
+ Style/EmptyElse:
100
+ Enabled: false
101
+
102
+ # Justification:
103
+ #
104
+ # We've generally prefered this, and honestly, I find
105
+ # this often makes readability much clearer to include
106
+ # it.
107
+ Style/RedundantSelf:
108
+ Enabled: false
109
+
110
+ Style/StringLiterals:
111
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ v1.3.1, 2025-08-06
2
+ -------------------
3
+ * [BUGFIX] Evaluator fix for Not expressions
4
+
5
+ v1.3.0, 2022-02-01
6
+ -------------------
7
+ * [BUGFIX] Redesign FunctionResolver to better support other timezones
8
+
1
9
  v1.2.8, 2021-08-11
2
10
  -------------------
3
11
  * [IMPROVEMENT] all() function
data/Gemfile CHANGED
@@ -1,3 +1,3 @@
1
- source 'http://rubygems.org'
1
+ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
data/Rakefile CHANGED
@@ -15,7 +15,7 @@ rule '.rb' => '.y' do |t|
15
15
  end
16
16
 
17
17
  desc "Compile the racc parser from the grammar"
18
- task :compile => ["lib/sparkql/parser.rb", "grammar"]
18
+ task compile: ["lib/sparkql/parser.rb", "grammar"]
19
19
 
20
20
  desc "Generate grammar Documenation"
21
21
  task :grammar do
@@ -27,5 +27,4 @@ Rake::Task[:test].prerequisites.unshift "lib/sparkql/parser.rb"
27
27
  Rake::Task[:test].prerequisites.unshift "grammar"
28
28
 
29
29
  desc 'Default: run unit tests.'
30
- task :default => :test
31
-
30
+ task default: :test
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.8
1
+ 1.3.1
@@ -1,87 +1,84 @@
1
1
  module Sparkql
2
+ class ErrorsProcessor
3
+ attr_accessor :errors
2
4
 
3
- class ErrorsProcessor
4
- attr_accessor :errors
5
+ def initialize(errors = [])
6
+ @errors = Array(errors)
7
+ end
5
8
 
6
- def initialize( errors = [] )
7
- @errors = Array(errors)
8
- end
9
+ # true if the error stack contains at least one error
10
+ def errors?
11
+ @errors.size.positive?
12
+ end
9
13
 
10
- # true if the error stack contains at least one error
11
- def errors?
12
- @errors.size > 0
13
- end
14
+ # true if there is at least one error of status :status in the error stack
15
+ def errors_by_status?(status)
16
+ @errors.each do |error|
17
+ return true if status == error.status
18
+ end
19
+ false
20
+ end
14
21
 
15
- # true if there is at least one error of status :status in the error stack
16
- def errors_by_status?( status )
17
- @errors.each do | error |
18
- return true if status == error.status
22
+ # true if there is at least one :fatal error in the error stack
23
+ def fatal_errors?
24
+ errors_by_status? :fatal
19
25
  end
20
- false
21
- end
22
26
 
23
- # true if there is at least one :fatal error in the error stack
24
- def fatal_errors?
25
- errors_by_status? :fatal
26
- end
27
+ # true if there is at least one :dropped error in the error stack
28
+ def dropped_errors?
29
+ errors_by_status? :dropped
30
+ end
27
31
 
28
- # true if there is at least one :dropped error in the error stack
29
- def dropped_errors?
30
- errors_by_status? :dropped
32
+ # true if there is at least one :recovered error in the error stack
33
+ def recovered_errors?
34
+ errors_by_status? :recovered
35
+ end
31
36
  end
32
37
 
33
- # true if there is at least one :recovered error in the error stack
34
- def recovered_errors?
35
- errors_by_status? :recovered
36
- end
38
+ class ParserError
39
+ attr_accessor :token, :token_index, :expression, :message, :status, :recovered_as,
40
+ :sparkql, :nested_errors
41
+ attr_writer :syntax, :constraint
37
42
 
38
- end
43
+ def initialize(error_hash = {})
44
+ @token = error_hash[:token]
45
+ @token_index = error_hash[:token_index]
46
+ @expression = error_hash[:expression]
47
+ @message = error_hash[:message]
48
+ @status = error_hash[:status]
49
+ @recovered_as = error_hash[:recovered_as]
50
+ @recovered_as = error_hash[:recovered_as]
51
+ @sparkql = error_hash[:sparkql]
52
+ @nested_errors = error_hash[:nested_errors]
53
+ self.syntax = error_hash[:syntax] != false
54
+ self.constraint = error_hash[:constraint] == true
55
+ end
39
56
 
40
- class ParserError
41
- attr_accessor :token, :token_index, :expression, :message, :status, :recovered_as,
42
- :sparkql, :nested_errors
43
- attr_writer :syntax, :constraint
57
+ def syntax?
58
+ @syntax
59
+ end
44
60
 
45
- def initialize(error_hash={})
46
- @token = error_hash[:token]
47
- @token_index = error_hash[:token_index]
48
- @expression = error_hash[:expression]
49
- @message = error_hash[:message]
50
- @status = error_hash[:status]
51
- @recovered_as = error_hash[:recovered_as]
52
- @recovered_as = error_hash[:recovered_as]
53
- @sparkql = error_hash[:sparkql]
54
- @nested_errors = error_hash[:nested_errors]
55
- self.syntax= error_hash[:syntax] == false ? false : true
56
- self.constraint= error_hash[:constraint] == true
57
- end
58
-
59
- def syntax?
60
- @syntax
61
- end
62
-
63
- def constraint?
64
- @constraint
65
- end
61
+ def constraint?
62
+ @constraint
63
+ end
66
64
 
67
- def to_s
68
- str = case @status
69
- # Do nothing. Dropping the expressions isn't special
70
- when :dropped then "Dropped: "
71
- # Fatal errors cannot be recovered from, and should cause anaylisis or
72
- # compilation to stop.
73
- when :fatal then "Fatal: "
74
- # Recovered errors are those that are syntatically
75
- # or symantically incorrect, but are ones that we could "guess" at the
76
- # intention
77
- when :recovered then
78
- "Recovered as #{@recovered_as}: "
79
- else ""
80
- end
81
- str += "<#{@token}> in " unless @token.nil?
82
- str += "<#{@expression}>: #{@message}."
83
- str
65
+ def to_s
66
+ str = case @status
67
+ # Do nothing. Dropping the expressions isn't special
68
+ when :dropped then "Dropped: "
69
+ # Fatal errors cannot be recovered from, and should cause anaylisis or
70
+ # compilation to stop.
71
+ when :fatal then "Fatal: "
72
+ # Recovered errors are those that are syntatically
73
+ # or symantically incorrect, but are ones that we could "guess" at the
74
+ # intention
75
+ when :recovered
76
+ "Recovered as #{@recovered_as}: "
77
+ else ""
78
+ end
79
+ str += "<#{@token}> in " unless @token.nil?
80
+ str += "<#{@expression}>: #{@message}."
81
+ str
82
+ end
84
83
  end
85
84
  end
86
-
87
- end
@@ -4,148 +4,152 @@
4
4
  # fields. Plus, it has some optimizations built in to skip the processing for
5
5
  # any expressions that don't contribute to the net result of the filter.
6
6
  class Sparkql::Evaluator
7
-
8
- # The struct here mimics some of the parser information about an expression,
9
- # but should not be confused for an expression. Nodes reduce the expressions
10
- # to a result based on conjunction logic, and only one exists per block group.
11
- Node = Struct.new(
12
- :level,
13
- :block_group,
14
- :conjunction,
15
- :conjunction_level,
16
- :match,
17
- :good_ors,
18
- :expressions,
19
- :unary)
20
-
21
7
  attr_reader :processed_count
22
8
 
23
- def initialize expression_resolver
9
+ def initialize(expression_resolver)
24
10
  @resolver = expression_resolver
25
11
  end
26
12
 
27
13
  def evaluate(expressions)
28
14
  @processed_count = 0
29
- @index = Node.new(0, 0, "And", 0, true, false, 0, nil)
30
- @groups = [@index]
31
- expressions.each do |expression|
32
- handle_group(expression)
33
- adjust_expression_for_dropped_field(expression)
34
- check_for_good_ors(expression)
35
- next if skip?(expression)
36
- evaluate_expression(expression)
15
+ levels = {}
16
+ block_groups = {}
17
+
18
+ build_structures(levels, block_groups, expressions)
19
+
20
+ final_result = process_structures(levels, block_groups)
21
+ # If we didn't process anything, we consider that a success
22
+ if final_result.nil?
23
+ final_result = true
37
24
  end
38
- cleanup
39
- return @index[:match]
25
+
26
+ final_result
40
27
  end
41
28
 
42
29
  private
43
30
 
44
- # prepare the group stack for the next expression
45
- def handle_group(expression)
46
- if @index[:block_group] == expression[:block_group]
47
- # Noop
48
- elsif @index[:block_group] < expression[:block_group]
49
- @index = new_group(expression)
50
- @groups.push(@index)
51
- else
52
- # Turn the group into an expression, resolve down to previous group(s)
53
- smoosh_group(expression)
54
- end
55
- end
31
+ # Take all the expressions and organize them into "chunks" appropriate for
32
+ # evaluation. Each block group should process it's expressions, and every
33
+ # block group injects itself as a placeholder expression in the block group a
34
+ # level above it.
35
+ #
36
+ # When no block groups exist above, we must stub one out for processing.
37
+ def build_structures(levels, block_groups, expressions)
38
+ expressions.each do |expression|
39
+ level = expression[:level]
40
+ block = expression[:block_group]
41
+ block_group = block_groups[block]
56
42
 
57
- # Here's the real meat. We use an internal stack to represent the result of
58
- # each block_group. This logic is re-used when merging the final result of one
59
- # block group with the previous.
60
- def evaluate_expression(expression)
61
- @processed_count += 1
62
- evaluate_node(expression, @resolver.resolve(expression))
63
- end
64
- def evaluate_node(node, result)
65
- if result == :drop
66
- @dropped_expression = node
67
- return result
68
- end
69
- if node[:unary] == "Not"
70
- result = !result
71
- end
72
- if node[:conjunction] == 'Not' &&
73
- (node[:conjunction_level] == node[:level] ||
74
- node[:conjunction_level] == @index[:level])
75
- @index[:match] = !result if @index[:match]
76
- elsif node[:conjunction] == 'And' || @index[:expressions] == 0
77
- @index[:match] = result if @index[:match]
78
- elsif node[:conjunction] == 'Or' && result
79
- @index[:match] = result
80
- end
81
- @index[:expressions] += 1
82
- result
83
- end
43
+ unless block_group
44
+ block_groups[block] ||= block_builder(expression, level)
45
+ block_group = block_groups[block]
46
+ levels[level] ||= []
47
+ levels[level] << block
84
48
 
85
- # Optimization logic, once we find any set of And'd expressions that pass and
86
- # run into an Or at the same level, we can skip further processing at that
87
- # level.
88
- def check_for_good_ors(expression)
89
- if expression[:conjunction] == 'Or'
90
- good_index = @index
91
- unless expression[:conjunction_level] == @index[:level]
92
- good_index = nil
93
- # Well crap, now we need to go back and find that level by hand
94
- @groups.reverse_each do |i|
95
- if i[:level] == expression[:conjunction_level]
96
- good_index = i
49
+ # When dealing with Not expression conjunctions at the block level,
50
+ # it's far simpler to convert it into the equivalent "And Not"
51
+ if block_group[:conjunction] == "Not"
52
+ block_group[:unary] = "Not"
53
+ block_group[:conjunction] = "And"
54
+ end
55
+
56
+ # Every block group _must_ be seen as an expression in another block
57
+ # group.This aids in final resolution order when processing the levels
58
+ #
59
+ # This is even true if there's only one block group. We always end up
60
+ # with a level -1 here to turn the top level expressions into a block
61
+ # group for processing.
62
+ current_level = level
63
+ while current_level >= 0
64
+ current_level -= 1
65
+ levels[current_level] ||= []
66
+ last_block_group_id = levels[current_level].last
67
+ if last_block_group_id
68
+ block_groups[last_block_group_id][:expressions] << block_group
69
+ break
70
+ else
71
+ block_id = "placeholder_for_#{block}_#{current_level}"
72
+ placeholder_block = block_builder(block_group, current_level)
73
+ placeholder_block[:expressions] << block_group
74
+
75
+ levels[current_level] << block_id
76
+ block_groups[block_id] = placeholder_block
97
77
  end
98
78
  end
99
79
  end
100
- if !good_index.nil? && good_index[:expressions] > 0 && good_index[:match]
101
- good_index[:good_ors] = true
102
- end
80
+
81
+ block_group[:expressions] << expression
103
82
  end
104
83
  end
105
84
 
106
- # We can skip further expression processing when And-d with a false expression
107
- # or a "good Or" was already encountered.
108
- def skip?(expression)
109
- @index[:good_ors] ||
110
- !@index[:match] && expression[:conjunction] == 'And'
111
- end
85
+ # Starting from the deepest levels, we process block groups expressions and
86
+ # reduce the block group to a result. This result is used in our placeholder
87
+ # block groups at levels above, ending in a single final result.
88
+ def process_structures(levels, block_groups)
89
+ final_result = nil
112
90
 
113
- def new_group(expression)
114
- Node.new(expression[:level], expression[:block_group],
115
- expression[:conjunction], expression[:conjunction_level],
116
- true, false, 0, nil)
117
- end
91
+ # Now go through each level starting with the deepest and working back up.
92
+ levels.keys.sort.reverse.each do |level|
93
+ # Process each block group at this level and resolve the expressions in the group
94
+ levels[level].each do |block|
95
+ block_group = block_groups[block]
118
96
 
119
- # When the last expression was dropped, we need to repair the filter by
120
- # stealing the conjunction of that dropped field.
121
- def adjust_expression_for_dropped_field(expression)
122
- if @dropped_expression.nil?
123
- return
124
- elsif @dropped_expression[:block_group] == expression[:block_group]
125
- expression[:conjunction] = @dropped_expression[:conjunction]
126
- expression[:conjunction_level] = @dropped_expression[:conjunction_level]
127
- end
128
- @dropped_expression = nil
129
- end
97
+ block_result = nil
98
+ block_group[:expressions].each do |expression|
99
+ # If we encounter any or's in the same block group, we can cheat at
100
+ # resolving the rest, if we are at a true
101
+ if block_result && expression[:conjunction] == 'Or'
102
+ break
103
+ end
104
+
105
+ expression_result = if expression.key?(:result)
106
+ # This is a reduced block group, just pass on
107
+ # the result
108
+ expression[:result]
109
+ else
110
+ @processed_count += 1
111
+ @resolver.resolve(expression) # true, false, :drop
112
+ end
113
+ next if expression_result == :drop
114
+
115
+ if expression[:unary] == "Not"
116
+ expression_result = !expression_result
117
+ end
130
118
 
131
- # This is similar to the cleanup step, but happens when we return from a
132
- # nesting level. Before we can proceed, we need wrap up the result of the
133
- # nested group.
134
- def smoosh_group(expression)
135
- until @groups.last[:block_group] == expression[:block_group]
136
- last = @groups.pop
137
- @index = @groups.last
138
- evaluate_node(last, last[:match])
119
+ if block_result.nil?
120
+ block_result = expression_result
121
+ next
122
+ end
123
+
124
+ case expression[:conjunction]
125
+ when 'Not'
126
+ block_result &= !expression_result
127
+ when 'And'
128
+ block_result &= expression_result
129
+ when 'Or'
130
+ block_result |= expression_result
131
+ else
132
+ # Not a supported conjunction. We skip over this for backwards
133
+ # compatibility.
134
+ end
135
+ end
136
+
137
+ # block_group.delete(:expressions)
138
+ block_group[:result] = block_result
139
+ final_result = block_result
140
+ end
139
141
  end
142
+
143
+ final_result
140
144
  end
141
145
 
142
- # pop off the group stack, evaluating each group with the previous as we go.
143
- def cleanup
144
- while @groups.size > 1
145
- last = @groups.pop
146
- @index = @groups.last
147
- evaluate_node(last, last[:match])
148
- end
149
- @groups.last[:match]
146
+ def block_builder(expressionable, level)
147
+ {
148
+ conjunction: expressionable[:conjunction],
149
+ conjunction_level: expressionable[:conjunction_level],
150
+ level: level,
151
+ expressions: [],
152
+ result: nil
153
+ }
150
154
  end
151
155
  end
@@ -1,17 +1,16 @@
1
1
  # Base class for handling expression resolution
2
2
  class Sparkql::ExpressionResolver
3
-
4
3
  # Accepted results from the resolve method:
5
4
  # * true and false reflect the expression's boolean result (as all expressions
6
5
  # should).
7
6
  # * :drop is a special symbol indicating that the expression should be omitted
8
7
  # from the filter. Special rules apply for a dropped expression, such as
9
8
  # keeping the conjunction of the dropped expression.
10
- VALID_RESULTS = [true, false, :drop]
9
+ VALID_RESULTS = [true, false, :drop].freeze
11
10
 
12
11
  # Evaluate the result of this expression. Allows for any of the values in
13
12
  # VALID_RESULTS
14
- def resolve(expression)
13
+ def resolve(_expression)
15
14
  true
16
15
  end
17
16
  end