sparkql 1.3.2 → 1.3.4

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: d4f82b65f3fa050b24c53c7c5cb1e4a113d99e5e4ada4510695179c100aac711
4
- data.tar.gz: 6fb9758b1883beadbbf9f864a9b1b782bfbc6f29b1a0061f9f66f33e1a0ed0b3
3
+ metadata.gz: 995514ae40305cedb2116dafc6c98b8efb534ae3d2f31c9d48e86421e3f5c10c
4
+ data.tar.gz: 6162d29fb4520941edb2b53742d966fdf373418b2745f6fd1d8c1fa79432b896
5
5
  SHA512:
6
- metadata.gz: 9752471d2b83c520c9ac52fa6fbc0219ffd914648bd8801f7953f41799a31ecd3de242e9a62e89cf5a7752c29337e85b14cba254779583ad914ac3cc4c06dc5b
7
- data.tar.gz: 3ccd9092e28b7fd3064768deb46eb4d7d5205be213adad251bb568958d4975e109080a74759613fb6c0e425a0ddda0ca7af37253b71bbe2ce18fd57891e0f6e3
6
+ metadata.gz: c39c06894d6a7b298d4fafb3c578e6d203543c77c68ad0cf8acb9ec943c00c656e38529a7e6b2afe4cc3177b5a59b078c7ce4c193a08dea0f575bd62ee0495d7
7
+ data.tar.gz: fbbba23b099146c85f3c15fb82ea3bbc93fe989b156752a80137a2212b2ed64f4cf50ecb99bf06b59914f1e1f8996a65155c9cde11c025702e6b8504b083cc93
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ v1.3.4, 20265-01-20
2
+ * [BUGFIX] Validate limit for days and weekdays
3
+
4
+ v1.3.3, 2025-08-12
5
+ -------------------
6
+ * [BUGFIX] Evaluator fix for Not regression
7
+
1
8
  v1.3.2, 2025-08-06
2
9
  -------------------
3
10
  * [BUGFIX] More Evaluator fixes
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.3.2
1
+ 1.3.4
@@ -1,9 +1,12 @@
1
+
1
2
  # Using an instance of ExpressionResolver to resolve the individual expressions,
2
3
  # this class will evaluate the rest of a parsed sparkql string to true or false.
3
4
  # Namely, this class will handle all the nesting, boolean algebra, and dropped
4
5
  # fields. Plus, it has some optimizations built in to skip the processing for
5
6
  # any expressions that don't contribute to the net result of the filter.
6
7
  class Sparkql::Evaluator
8
+ include Sparkql::Token
9
+
7
10
  attr_reader :processed_count
8
11
 
9
12
  def initialize(expression_resolver)
@@ -40,6 +43,11 @@ class Sparkql::Evaluator
40
43
  block = expression[:block_group]
41
44
  block_group = block_groups[block]
42
45
 
46
+ if expression[:conjunction] == NOT && expression[:conjunction_level] == level
47
+ expression[:conjunction] = AND
48
+ expression[:unary] = NOT
49
+ end
50
+
43
51
  unless block_group
44
52
  block_groups[block] ||= block_builder(expression, level)
45
53
  block_group = block_groups[block]
@@ -48,9 +56,9 @@ class Sparkql::Evaluator
48
56
 
49
57
  # When dealing with Not expression conjunctions at the block level,
50
58
  # 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"
59
+ if block_group[:conjunction] == NOT
60
+ block_group[:unary] = NOT
61
+ block_group[:conjunction] = AND
54
62
  end
55
63
 
56
64
  # Every block group _must_ be seen as an expression in another block
@@ -100,7 +108,7 @@ class Sparkql::Evaluator
100
108
  block_group[:expressions].each do |expression|
101
109
  # If we encounter any or's in the same block group, we can cheat at
102
110
  # resolving the rest, if we are at a true
103
- if block_result && expression[:conjunction] == 'Or'
111
+ if block_result && expression[:conjunction] == OR
104
112
  break
105
113
  end
106
114
 
@@ -114,7 +122,7 @@ class Sparkql::Evaluator
114
122
  end
115
123
  next if expression_result == :drop
116
124
 
117
- if expression[:unary] == "Not"
125
+ if expression[:unary] == NOT
118
126
  expression_result = !expression_result
119
127
  end
120
128
 
@@ -124,11 +132,11 @@ class Sparkql::Evaluator
124
132
  end
125
133
 
126
134
  case expression[:conjunction]
127
- when 'Not'
135
+ when NOT
128
136
  block_result &= !expression_result
129
- when 'And'
137
+ when AND
130
138
  block_result &= expression_result
131
- when 'Or'
139
+ when OR
132
140
  block_result |= expression_result
133
141
  else
134
142
  # Not a supported conjunction. We skip over this for backwards
@@ -136,7 +144,7 @@ class Sparkql::Evaluator
136
144
  end
137
145
  end
138
146
 
139
- # block_group.delete(:expressions)
147
+ block_group.delete(:expressions)
140
148
  block_group[:result] = block_result
141
149
  final_result = block_result
142
150
  end
@@ -21,6 +21,7 @@ module Sparkql
21
21
  VALID_REGEX_FLAGS = ['', 'i'].freeze
22
22
  MIN_DATE_TIME = Time.new(1970, 1, 1, 0, 0, 0, '+00:00').iso8601
23
23
  MAX_DATE_TIME = Time.new(9999, 12, 31, 23, 59, 59, '+00:00').iso8601
24
+ MAX_DAYS = (365 * 1000).freeze # 1000 years ought to cover most cases
24
25
  VALID_CAST_TYPES = %i[field character decimal integer].freeze
25
26
 
26
27
  SUPPORTED_FUNCTIONS = {
@@ -43,16 +44,16 @@ module Sparkql
43
44
  regex: {
44
45
  args: [:character],
45
46
  opt_args: [{
46
- type: :character,
47
- default: ''
48
- }],
47
+ type: :character,
48
+ default: ''
49
+ }],
49
50
  return_type: :character
50
51
  },
51
52
  substring: {
52
53
  args: [%i[field character], :integer],
53
54
  opt_args: [{
54
- type: :integer
55
- }],
55
+ type: :integer
56
+ }],
56
57
  resolve_for_type: true,
57
58
  return_type: :character
58
59
  },
@@ -526,6 +527,15 @@ module Sparkql
526
527
 
527
528
  # Offset the current timestamp by a number of days
528
529
  def days(number_of_days)
530
+ if number_of_days.abs > MAX_DAYS
531
+ @errors << Sparkql::ParserError.new(token: number_of_days,
532
+ message: "Function call 'days' max offset #{MAX_DAYS} days",
533
+ status: :fatal,
534
+ syntax: false,
535
+ constraint: true)
536
+ return
537
+ end
538
+
529
539
  # date calculated as the offset from midnight tommorrow. Zero will provide values for all times
530
540
  # today.
531
541
  d = current_date + number_of_days
@@ -536,6 +546,15 @@ module Sparkql
536
546
  end
537
547
 
538
548
  def weekdays(number_of_days)
549
+ if number_of_days.abs > MAX_DAYS
550
+ @errors << Sparkql::ParserError.new(token: number_of_days,
551
+ message: "Function call 'weekdays' max offset #{MAX_DAYS} days",
552
+ status: :fatal,
553
+ syntax: false,
554
+ constraint: true)
555
+ return
556
+ end
557
+
539
558
  today = current_date
540
559
  weekend_start = today.saturday? || today.sunday?
541
560
  direction = number_of_days.positive? ? 1 : -1
data/lib/sparkql/token.rb CHANGED
@@ -24,8 +24,15 @@ module Sparkql::Token
24
24
  NULL = /NULL|null|Null/.freeze
25
25
  # Reserved words
26
26
  RANGE_OPERATOR = 'Bt'.freeze
27
- EQUALITY_OPERATORS = %w[Eq Ne].freeze
27
+ EQUAL = 'Eq'.freeze
28
+ NOT_EQUAL = 'Ne'.freeze
29
+ EQUALITY_OPERATORS = [EQUAL, NOT_EQUAL].freeze
30
+
28
31
  OPERATORS = %w[Gt Ge Lt Le] + EQUALITY_OPERATORS
29
- UNARY_CONJUNCTIONS = ['Not'].freeze
30
- CONJUNCTIONS = %w[And Or].freeze
32
+
33
+ NOT = 'Not'.freeze
34
+ AND = 'And'.freeze
35
+ OR = 'Or'.freeze
36
+ UNARY_CONJUNCTIONS = [NOT].freeze
37
+ CONJUNCTIONS = [AND, OR].freeze
31
38
  end
@@ -64,39 +64,93 @@ class EvaluatorTest < Test::Unit::TestCase
64
64
  def test_dropped_field_handling
65
65
  assert sample("Test Eq 'Drop' And Test Eq true")
66
66
  assert !sample("Test Eq 'Drop' And Test Eq false")
67
- assert !sample("Test Eq 'Drop' Or Test Eq false")
67
+
68
68
  assert sample("Test Eq 'Drop' Or Test Eq true")
69
+ assert !sample("Test Eq 'Drop' Or Test Eq false")
70
+
69
71
  assert sample("Test Eq false And Test Eq 'Drop' Or Test Eq true")
72
+ assert !sample("Test Eq false And Test Eq 'Drop' Or Test Eq false")
73
+
70
74
  assert sample("Test Eq false Or (Test Eq 'Drop' And Test Eq true)")
75
+ assert !sample("Test Eq false Or (Test Eq 'Drop' And Test Eq false)")
76
+
77
+ assert sample("Test Eq false Or (Not Test Eq 'Drop' And Test Eq true)")
78
+ assert !sample("Test Eq false Or (Not Test Eq 'Drop' And Test Eq false)")
79
+
80
+ assert sample("Test Eq true Not Test Eq 'Drop' And Test Eq true")
81
+ assert !sample("Test Eq true Not Test Eq 'Drop' And Test Eq false")
82
+ assert !sample("Test Eq false Not Test Eq 'Drop' And Test Eq false")
83
+
84
+ assert sample("Test Eq true And Test Eq 'Drop' Not Test Eq false")
85
+ assert !sample("Test Eq true And Test Eq 'Drop' Not Test Eq true")
86
+ assert !sample("Test Eq true And Test Eq 'Drop' Not Test Eq true")
87
+
88
+ assert sample("Test Eq true Not (Test Eq 'Drop' And Test Eq false)")
89
+ assert !sample("Test Eq true Not (Test Eq 'Drop' And Test Eq true)")
90
+ assert !sample("Test Eq true Not (Test Eq 'Drop' And Test Eq true)")
71
91
  end
72
92
 
73
93
  def test_nesting
74
94
  assert sample("Test Eq true Or (Test Eq true) And Test Eq false And (Test Eq true)")
95
+ assert sample("Test Eq true Or (Test Eq false) And Test Eq false And (Test Eq false)")
96
+ assert sample("Test Eq false Or (Test Eq true) And Test Eq true And (Test Eq true)")
97
+ assert !sample("Test Eq false Or (Test Eq false) And Test Eq false And (Test Eq false)")
98
+ assert !sample("Test Eq false Or (Test Eq true) And Test Eq false And (Test Eq false)")
99
+ assert !sample("Test Eq false Or (Test Eq false) And Test Eq true And (Test Eq false)")
100
+ assert !sample("Test Eq false Or (Test Eq false) And Test Eq false And (Test Eq true)")
101
+
75
102
  assert sample("Test Eq true Or ((Test Eq false) And Test Eq false) And (Test Eq false)")
76
103
  assert sample("(Test Eq false Or Test Eq true) Or (Test Eq false Or Test Eq false)")
77
104
  assert sample("(Test Eq true And Test Eq true) Or (Test Eq false)")
78
105
  assert sample("(Test Eq true And Test Eq true) Or (Test Eq false And Test Eq true)")
79
106
  assert !sample("(Test Eq false And Test Eq true) Or (Test Eq false)")
107
+
80
108
  assert sample("Test Eq true And ((Test Eq true And Test Eq false) Or Test Eq true)")
81
109
  assert !sample("Test Eq true And ((Test Eq true And Test Eq false) Or Test Eq false) And Test Eq true")
82
110
  assert !sample("Test Eq true And ((Test Eq true And Test Eq false) Or Test Eq false) Or Test Eq false")
83
111
  assert sample("Test Eq true And ((Test Eq true And Test Eq false) Or Test Eq false) Or Test Eq true")
84
112
  assert !sample("(Test Eq true Or Test Eq true) And Test Eq false")
85
113
  assert !sample("(Test Eq true Or Test Eq true) And (Test Eq false)")
114
+
86
115
  assert sample("(Test Eq true Or Test Eq true) And (Test Eq false Or Test Eq true)")
87
116
  assert !sample("(Test Eq true Or Test Eq true) And (Test Eq false Or Test Eq false)")
117
+
118
+ assert sample("(Test Eq true) Not Test Eq false And (Test Eq true)")
119
+ assert !sample("(Test Eq true) Not Test Eq true And (Test Eq true)")
120
+ assert !sample("(Test Eq false) Not Test Eq false And (Test Eq true)")
121
+ assert !sample("(Test1 Eq true) Not Test2 Eq false And (Test3 Eq false)")
88
122
  end
89
123
 
90
124
  def test_nots
91
125
  assert sample("Test Eq true Not Test Eq false")
92
126
  assert !sample("Test Eq true Not Test Eq true")
127
+ assert !sample("Test Eq false Not Test Eq true")
128
+ assert !sample("Test Eq false Not Test Eq false")
129
+
130
+ assert sample("Test Eq true And Test Eq true Not Test Eq false")
131
+ assert !sample("Test Eq false And Test Eq true Not Test Eq false")
132
+ assert !sample("Test Eq true And Test Eq true Not Test Eq true")
133
+ assert !sample("Test Eq true And Test Eq false Not Test Eq false")
134
+
93
135
  assert sample("Test Eq true Not (Test Eq false Or Test Eq false)")
94
- assert sample("Test Eq true Not (Test Eq false And Test Eq false)")
95
136
  assert !sample("Test Eq true Not (Test Eq false Or Test Eq true)")
96
137
  assert !sample("Test Eq true Not (Test Eq true Or Test Eq false)")
97
- assert !sample("Test Eq true Not (Not Test Eq false)")
98
- assert !sample("Test Eq false And Test Eq true Not Test Eq false")
138
+ assert !sample("Test Eq true Not (Test Eq true Or Test Eq true)")
139
+ assert !sample("Test Eq false Not (Test Eq false Or Test Eq false)")
140
+
141
+ assert sample("Test Eq true Not (Test Eq false And Test Eq false)")
142
+ assert sample("Test Eq true Not (Test Eq true And Test Eq false)")
143
+ assert sample("Test Eq true Not (Test Eq false And Test Eq true)")
144
+ assert !sample("Test Eq true Not (Test Eq true And Test Eq true)")
145
+ assert !sample("Test Eq false Not (Test Eq false And Test Eq false)")
146
+
147
+ assert sample("Test Eq true Not (Test Eq false Or Test Eq false) And (Test Eq true Or Test Eq false)")
148
+ assert sample("Test Eq true Not (Test Eq false Or Test Eq false) And (Test Eq false Or Test Eq true)")
149
+ assert sample("Test Eq true Not (Test Eq false Or Test Eq false) And (Test Eq true Or Test Eq true)")
99
150
  assert !sample("Test Eq true Not (Test Eq false Or Test Eq false) And (Test Eq false Or Test Eq false)")
151
+ assert !sample("Test Eq true Not (Test Eq false Or Test Eq true) And (Test Eq true Or Test Eq false)")
152
+ assert !sample("Test Eq true Not (Test Eq true Or Test Eq false) And (Test Eq true Or Test Eq false)")
153
+ assert !sample("Test Eq false Not (Test Eq false Or Test Eq false) And (Test Eq true Or Test Eq false)")
100
154
  end
101
155
 
102
156
  def test_unary_nots
@@ -109,17 +163,14 @@ class EvaluatorTest < Test::Unit::TestCase
109
163
 
110
164
  def test_unary_double_nots
111
165
  assert sample("Not (Not(Not Test Eq true))")
166
+ assert !sample("Not (Not(Not Test Eq false))")
167
+
168
+ assert sample("Test Eq true Not (Not Test Eq true)")
169
+ assert !sample("Test Eq true Not (Not Test Eq false)")
170
+ assert !sample("Test Eq false Not (Not Test Eq true)")
112
171
  end
113
172
 
114
173
  def test_examples
115
- # This one is based on a real life example that had problems.
116
- #
117
- # CurrentPrice Bt 130000.00,180000.00 And PropertySubType Eq 'Single Family Residence' And
118
- # SchoolDistrict Eq 'Byron Center','Grandville','Jenison' And MlsStatus Eq 'Active' And
119
- # BathsTotal Bt 1.50,9999.00 And BedsTotal Bt 3,99 And PropertyType Eq 'A'
120
- # Not "Garage"."Garage2" Eq 'No' And "Pool"."OutdoorAbove" Eq true
121
- # And "Pool"."OutdoorInground" Eq true Not "Substructure"."Michigan Basement" Eq true
122
-
123
174
  assert !sample("Test Eq false And Test Eq true And " \
124
175
  "Test Eq false And Test Eq true And " \
125
176
  "Test Eq true And Test Eq true And Test Eq true " \
@@ -445,6 +445,24 @@ class FunctionResolverTest < Test::Unit::TestCase
445
445
  end
446
446
  end
447
447
 
448
+ test 'days - exceed max' do
449
+ f = FunctionResolver.new('days',
450
+ [{ type: :integer, value: 365_001 }],
451
+ current_timestamp: EXAMPLE_DATE)
452
+ f.validate
453
+ assert !f.errors?
454
+ assert_nil f.call
455
+ assert f.errors?, "function 'days' limit 365000"
456
+
457
+ f = FunctionResolver.new('days',
458
+ [{ type: :integer, value: -365_001 }],
459
+ current_timestamp: EXAMPLE_DATE)
460
+ f.validate
461
+ assert !f.errors?
462
+ assert_nil f.call
463
+ assert f.errors?, "function 'days' limit 365000"
464
+ end
465
+
448
466
  test 'weekdays()' do
449
467
  friday = Date.new(2012, 10, 19)
450
468
  saturday = Date.new(2012, 10, 20)
@@ -514,6 +532,24 @@ class FunctionResolverTest < Test::Unit::TestCase
514
532
  end
515
533
  end
516
534
 
535
+ test 'weekdays - exceed max' do
536
+ f = FunctionResolver.new('weekdays',
537
+ [{ type: :integer, value: 365_001 }],
538
+ current_timestamp: EXAMPLE_DATE)
539
+ f.validate
540
+ assert !f.errors?
541
+ assert_nil f.call
542
+ assert f.errors?, "function 'weekdays' limit 365000"
543
+
544
+ f = FunctionResolver.new('weekdays',
545
+ [{ type: :integer, value: -365_001 }],
546
+ current_timestamp: EXAMPLE_DATE)
547
+ f.validate
548
+ assert !f.errors?
549
+ assert_nil f.call
550
+ assert f.errors?, "function 'weekdays' limit 365000"
551
+ end
552
+
517
553
  test 'months()' do
518
554
  f = FunctionResolver.new('months',
519
555
  [{ type: :integer, value: 3 }],
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sparkql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wade McEwen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-07 00:00:00.000000000 Z
11
+ date: 2026-01-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: georuby