sneaql 0.0.8-java
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 +7 -0
- data/bin/sneaql +273 -0
- data/lib/sneaql.rb +284 -0
- data/lib/sneaql_lib/base.rb +224 -0
- data/lib/sneaql_lib/core.rb +346 -0
- data/lib/sneaql_lib/database_manager.rb +79 -0
- data/lib/sneaql_lib/database_prefs/redshift.rb +22 -0
- data/lib/sneaql_lib/database_prefs/sqlite.rb +12 -0
- data/lib/sneaql_lib/database_prefs/vertica.rb +21 -0
- data/lib/sneaql_lib/exceptions.rb +78 -0
- data/lib/sneaql_lib/expressions.rb +238 -0
- data/lib/sneaql_lib/lock_manager.rb +176 -0
- data/lib/sneaql_lib/parser.rb +89 -0
- data/lib/sneaql_lib/recordset.rb +97 -0
- data/lib/sneaql_lib/repo_manager.rb +95 -0
- data/lib/sneaql_lib/standard.rb +30 -0
- data/lib/sneaql_lib/standard_db_objects.rb +232 -0
- data/lib/sneaql_lib/step_manager.rb +60 -0
- metadata +131 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
module Sneaql
|
2
|
+
# Exceptions for SneaQL
|
3
|
+
module Exceptions
|
4
|
+
# Base error class for Sneaql
|
5
|
+
class BaseError < StandardError; end
|
6
|
+
|
7
|
+
# Exception used to to gracefully exit test
|
8
|
+
class SQLTestExitCondition < BaseError
|
9
|
+
def initialize(msg = "Exit condition met by test, this is not an error")
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Exception used to gracefully exit test step
|
15
|
+
class SQLTestStepExitCondition < BaseError
|
16
|
+
def initialize(msg = "Exit condition for this step has been met, this is not an error")
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Transform is locked by another process. This is a
|
22
|
+
# possibility when using the LockManager
|
23
|
+
class TransformIsLocked < BaseError
|
24
|
+
def initialize(msg = "This transform is locked by another process")
|
25
|
+
super
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Recordset check failure indicator
|
30
|
+
class RecordsetContainsInconsistentOrInvalidTypes < BaseError
|
31
|
+
def initialize(msg = "Recordsets must have identical keys in every record")
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Recordset check failure indicator
|
37
|
+
class RecordsetIsNotAnArray < BaseError
|
38
|
+
def initialize(msg = "Recordset must be an array of hashes with identical keys")
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# General error evaluating expression.
|
44
|
+
class ExpressionEvaluationError < BaseError
|
45
|
+
def initialize(msg = "Error evaluating expression")
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Comparison operator must be explicitly supported.
|
51
|
+
class InvalidComparisonOperator < BaseError
|
52
|
+
def initialize(msg = "Invalid or no comparison operator provided")
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Error raised during parser validation process
|
58
|
+
class StatementParsingError < BaseError
|
59
|
+
def initialize(msg = "General error parsing Sneaql tag and statement")
|
60
|
+
super
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Sneaql step files must not be empty
|
65
|
+
class NoStatementsFoundInFile < BaseError
|
66
|
+
def initialize(msg = "No statements found in step file.")
|
67
|
+
super
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Sneaql command tags must be formed correctly
|
72
|
+
class MalformedSneaqlCommandsInStep < BaseError
|
73
|
+
def initialize(msg = "Sneaql command tag is malformed.")
|
74
|
+
super
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Sneaql
|
4
|
+
module Core
|
5
|
+
# Handles variables, expression evaluation, and comparisons.
|
6
|
+
# A single ExpressionHandler is created per transform. This
|
7
|
+
# object will get passed around to the various commands as well
|
8
|
+
# as other manager objects attached to the transform class.
|
9
|
+
class ExpressionHandler
|
10
|
+
# @param [Hash] environment_variables pass in a set of ENV
|
11
|
+
# @param [Logger] logger object otherwise will default to new Logger
|
12
|
+
def initialize(environment_variables, logger = nil)
|
13
|
+
@logger = logger ? logger : Logger.new(STDOUT)
|
14
|
+
@environment_variables = environment_variables
|
15
|
+
@session_variables = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param [String] var_name identifier for variable
|
19
|
+
# @param [String, Fixnum, Float] var_value value to store, expressions here will not be evaluated
|
20
|
+
def set_session_variable(var_name, var_value)
|
21
|
+
@logger.info("setting session var #{var_name} to #{var_value}")
|
22
|
+
raise "can't set environment variable #{var_name}" unless valid_session_variable_name?(var_name)
|
23
|
+
@session_variables[var_name] = var_value
|
24
|
+
end
|
25
|
+
|
26
|
+
# validates that this would make a suitable variable name
|
27
|
+
# @param [String] var_name
|
28
|
+
# @return [Boolean]
|
29
|
+
def valid_session_variable_name?(var_name)
|
30
|
+
r = (var_name.to_s.match(/^\w+$/) && !var_name.to_s.match(/env\_\w*/) && !var_name.to_s.match(/^\d+/)) ? true : false
|
31
|
+
@logger.debug "validating #{var_name} as valid variable identifier indicates #{r}"
|
32
|
+
r
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param [String] var_name identifier for variable
|
36
|
+
# @return [String, Fixnum, Float]
|
37
|
+
def get_session_variable(var_name)
|
38
|
+
@session_variables[var_name]
|
39
|
+
end
|
40
|
+
|
41
|
+
# convenience method, outputs all session variables to the logger
|
42
|
+
def output_all_session_variables
|
43
|
+
@logger.debug("current session variables: #{@session_variables}")
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param [String] var_name identifier for environment variable as defined in ENV
|
47
|
+
# @return [String]
|
48
|
+
def get_environment_variable(var_name)
|
49
|
+
@environment_variables[var_name]
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param [String] expression either a numeric constant, string constant in '',
|
53
|
+
# or reference to session or environment variable
|
54
|
+
# @return [String, Fixnum, Float]
|
55
|
+
def evaluate_expression(expression)
|
56
|
+
return expression unless expression.class == String
|
57
|
+
|
58
|
+
# reference to an environment variable
|
59
|
+
# :env_var_name or :ENV_var_name
|
60
|
+
# env variable references are case insensitive in this case
|
61
|
+
if expression =~ /\:env\_\w+/i
|
62
|
+
return @environment_variables[expression.gsub(/\:env\_/i, '').strip]
|
63
|
+
|
64
|
+
# reference to a variable
|
65
|
+
# ANSI dynamic SQL :var_name
|
66
|
+
# variable names are case sensitive
|
67
|
+
elsif expression =~ /\:\w+/
|
68
|
+
return @session_variables[expression.gsub(/\:/, '').strip]
|
69
|
+
|
70
|
+
# deprecated
|
71
|
+
elsif expression =~ /\{.*\}/
|
72
|
+
@logger.warn '{var_name} deprecated. use dynamic SQL syntax :var_name'
|
73
|
+
return @session_variables[expression.gsub(/\{|\}/, '').strip]
|
74
|
+
|
75
|
+
# string literal enclosed in single quotes
|
76
|
+
# only works for a single word... no whitespace allowed at this time
|
77
|
+
elsif expression =~ /\'.*\'/
|
78
|
+
return expression.delete("'").strip
|
79
|
+
|
80
|
+
# else assume it is a numeric literal
|
81
|
+
# need some better thinking here
|
82
|
+
else
|
83
|
+
return expression.strip
|
84
|
+
end
|
85
|
+
rescue => e
|
86
|
+
@logger.error("error evaluating expression: #{e.message}")
|
87
|
+
e.backtrace.each { |b| logger.error(b.to_s) }
|
88
|
+
raise Sneaql::Exceptions::ExpressionEvaluationError
|
89
|
+
end
|
90
|
+
|
91
|
+
# evaluates all expressions in a given SQL statement.
|
92
|
+
# replaces...
|
93
|
+
# environment variables in the form :env_HOSTNAME
|
94
|
+
# session variables in the form :variable_name
|
95
|
+
# session variables in the deprecated form {variable_name}
|
96
|
+
# @param [String] statement SQL statement to have all expressions evaluated
|
97
|
+
# @return [String] SQL statement with all variable references resolved
|
98
|
+
def evaluate_all_expressions(statement)
|
99
|
+
evaluate_session_variables(statement)
|
100
|
+
evaluate_environment_variables(statement)
|
101
|
+
evaluate_session_variables_braces(statement)
|
102
|
+
return statement
|
103
|
+
rescue => e
|
104
|
+
@logger.error "evaluation error #{e.message}"
|
105
|
+
e.backtrace.each { |b| logger.error b.to_s }
|
106
|
+
raise Sneaql::Exceptions::ExpressionEvaluationError
|
107
|
+
end
|
108
|
+
|
109
|
+
# evaluates all environment variables in a given SQL statement.
|
110
|
+
# replaces...
|
111
|
+
# environment variables in the form :env_HOSTNAME
|
112
|
+
# @param [String] statement SQL statement to have all environment variables evaluated
|
113
|
+
# @return [String] SQL statement with all variable references resolved
|
114
|
+
def evaluate_session_variables(statement)
|
115
|
+
# replaces :var_name in provided statement
|
116
|
+
@session_variables.keys.each do |k|
|
117
|
+
statement.gsub!(/\:#{k}/, @session_variables[k].to_s)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# evaluates all session variables in a given SQL statement.
|
122
|
+
# replaces...
|
123
|
+
# session variables in the form :variable_name
|
124
|
+
# @param [String] statement SQL statement to have all session variables evaluated
|
125
|
+
# @return [String] SQL statement with all variable references resolved
|
126
|
+
def evaluate_environment_variables(statement)
|
127
|
+
# replace env vars in the form :env_HOSTNAME
|
128
|
+
@environment_variables.keys.each do |e|
|
129
|
+
statement.gsub!(/\:env\_#{e}/i, @environment_variables[e])
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# evaluates all session variables in a given SQL statement.
|
134
|
+
# replaces...
|
135
|
+
# session variables in the deprecated form {variable_name}
|
136
|
+
# @param [String] statement SQL statement to have all deprecated form variable references evaluated
|
137
|
+
# @return [String] SQL statement with all variable references resolved
|
138
|
+
# @deprecated
|
139
|
+
def evaluate_session_variables_braces(statement)
|
140
|
+
# deprecated
|
141
|
+
@session_variables.keys.each do |k|
|
142
|
+
statement.gsub!(/\{#{k}\}/, @session_variables[k].to_s)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# validates that this would make a suitable reference at run time.
|
147
|
+
# checks to see this is single quoted string, :variable_name, {var_name) or number (1, 1.031, etc.)
|
148
|
+
# @param [String] expr value to check
|
149
|
+
def valid_expression_reference?(expr)
|
150
|
+
return expr.to_s.match(/(^\'.+\'$|^\:\w+$|^\{\w+\}$|^\d+$|^\d+\.\d*$)/) ? true : false
|
151
|
+
end
|
152
|
+
|
153
|
+
# Operators valid for expression comparison
|
154
|
+
# @return [Array<String>]
|
155
|
+
def valid_operators
|
156
|
+
['=', '!=', '>', '<', '>=', '<=', 'like', 'notlike']
|
157
|
+
end
|
158
|
+
|
159
|
+
# provides a standardized method of comparing two expressions.
|
160
|
+
# note that this only works for variables and constants.
|
161
|
+
# current version supports float, integer, and contigious strings.
|
162
|
+
# @param [String] operator comparison operator @see valid_operators
|
163
|
+
# @param [String] exp1 expression for left operand
|
164
|
+
# @param [String] exp2 expression for right operand
|
165
|
+
def compare_expressions(operator, exp1, exp2)
|
166
|
+
unless valid_operators.include?(operator)
|
167
|
+
raise Sneaql::Exceptions::InvalidComparisonOperator
|
168
|
+
end
|
169
|
+
|
170
|
+
@logger.debug "evaluating #{exp1} #{operator} #{exp2}"
|
171
|
+
|
172
|
+
# evaluate exps and coerce data types
|
173
|
+
coerced = coerce_data_types(
|
174
|
+
evaluate_expression(exp1),
|
175
|
+
evaluate_expression(exp2)
|
176
|
+
)
|
177
|
+
|
178
|
+
compare_values(operator, coerced[0], coerced[1])
|
179
|
+
end
|
180
|
+
|
181
|
+
# coerces the data types for both expressions to match for valid comparison
|
182
|
+
# @param [String, Float, Fixnum] exp1 expression for left operand
|
183
|
+
# @param [String, Float, Fixnum] exp2 expression for right operand
|
184
|
+
# @return [Array<Float, Fixnum, String>] returns array with both input expressions coerced to the same data type
|
185
|
+
def coerce_data_types(exp1, exp2)
|
186
|
+
# coerce data types to make for a good comparison
|
187
|
+
if exp1.class == exp2.class
|
188
|
+
nil # nothing to do... continue with comparison
|
189
|
+
elsif [exp1.class, exp2.class].include? Float
|
190
|
+
# if either is a float then make sure they are both floats
|
191
|
+
exp1 = exp1.to_f
|
192
|
+
exp2 = exp2.to_f
|
193
|
+
elsif [exp1.class, exp2.class].include? Fixnum
|
194
|
+
# otherwise... if one is an integer make them both integers
|
195
|
+
exp1 = exp1.to_i
|
196
|
+
exp2 = exp2.to_i
|
197
|
+
end
|
198
|
+
[exp1, exp2]
|
199
|
+
end
|
200
|
+
|
201
|
+
# performs the actual comparison between two values
|
202
|
+
# @param [String] operator comparison operator @see valid_operators
|
203
|
+
# @param [String] exp1 expression for left operand
|
204
|
+
# @param [String] exp2 expression for right operand
|
205
|
+
# @return [Boolean]
|
206
|
+
def compare_values(operator, exp1, exp2)
|
207
|
+
# below are all the valid comparison operators
|
208
|
+
@logger.debug("comparing #{exp1} #{operator} #{exp2}")
|
209
|
+
case operator
|
210
|
+
when '=' then return exp1 == exp2
|
211
|
+
when '!=' then return exp1 != exp2
|
212
|
+
when '>=' then return exp1 >= exp2
|
213
|
+
when '<=' then return exp1 <= exp2
|
214
|
+
when '>' then return exp1 > exp2
|
215
|
+
when '<' then return exp1 < exp2
|
216
|
+
when 'like' then return like_operator(exp1, exp2)
|
217
|
+
when 'notlike' then return !like_operator(exp1, exp2)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# performs SQL style LIKE comparison between inputs
|
222
|
+
# @param [String] left_operand
|
223
|
+
# @param [String] like_right_operand this will be the like expression
|
224
|
+
# @return [Boolean]
|
225
|
+
def like_operator(left_operand, like_right_operand)
|
226
|
+
#converts to string before comparison
|
227
|
+
return left_operand.to_s.match(wildcard_to_regex(like_right_operand.to_s)) ? true : false
|
228
|
+
end
|
229
|
+
|
230
|
+
# converts a SQL LIKE wildcard expression to a Regexp
|
231
|
+
# @param [String] wildcard like expression
|
232
|
+
# @return [Regexp] returns regexp object for use in match comparison
|
233
|
+
def wildcard_to_regex(wildcard)
|
234
|
+
Regexp.new("^#{wildcard}$".gsub('%','.*').gsub('_','.'))
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'jdbc_helpers'
|
2
|
+
|
3
|
+
module Sneaql
|
4
|
+
module Core
|
5
|
+
# manages transform locking operations using a standardized
|
6
|
+
# table for storing the locks.
|
7
|
+
class TransformLockManager
|
8
|
+
# set instance variables that will be used to manage the locks
|
9
|
+
def initialize(params, logger = nil)
|
10
|
+
@logger = logger ? logger : Logger.new(STDOUT)
|
11
|
+
@transform_name = params[:transform_name]
|
12
|
+
@transform_lock_id = params[:transform_lock_id]
|
13
|
+
@transform_lock_table = params[:transform_lock_table]
|
14
|
+
@jdbc_url = params[:jdbc_url]
|
15
|
+
@db_user = params[:db_user]
|
16
|
+
@db_pass = params[:db_pass]
|
17
|
+
@database_manager = Sneaql::Core.find_class(
|
18
|
+
:database,
|
19
|
+
params[:database]
|
20
|
+
).new
|
21
|
+
rescue => e
|
22
|
+
@logger.error e.message
|
23
|
+
e.backtrace.each { |b| @logger.error b}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Creates a connection in the current JDBC context
|
27
|
+
def create_jdbc_connection
|
28
|
+
JDBCHelpers::ConnectionFactory.new(
|
29
|
+
@jdbc_url,
|
30
|
+
@db_user,
|
31
|
+
@db_pass,
|
32
|
+
@logger
|
33
|
+
).connection
|
34
|
+
end
|
35
|
+
|
36
|
+
# Checks to see if the current transform is locked
|
37
|
+
# @return [Boolean]
|
38
|
+
def acquire_lock
|
39
|
+
# check to see if this transform is locked by
|
40
|
+
# another transform returns true if locked
|
41
|
+
jdbc_connection = create_jdbc_connection
|
42
|
+
|
43
|
+
# initialize lock value
|
44
|
+
lock_value = false
|
45
|
+
|
46
|
+
if @database_manager.supports_transactions == true
|
47
|
+
l = JDBCHelpers::Execute.new(
|
48
|
+
jdbc_connection,
|
49
|
+
@database_manager.begin_statement,
|
50
|
+
@logger
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
if @database_manager.supports_table_locking == true
|
55
|
+
l = JDBCHelpers::Execute.new(
|
56
|
+
jdbc_connection,
|
57
|
+
@database_manager.lock_table_statement(@transform_lock_table),
|
58
|
+
@logger
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
# query the number of rows which match the condition...
|
63
|
+
# should be 1 or 0... 1 indicating a lock
|
64
|
+
r = JDBCHelpers::SingleValueFromQuery.new(
|
65
|
+
jdbc_connection,
|
66
|
+
%(select
|
67
|
+
count(*)
|
68
|
+
from
|
69
|
+
#{@transform_lock_table}
|
70
|
+
where
|
71
|
+
transform_name='#{@transform_name}'
|
72
|
+
and
|
73
|
+
transform_lock_id!=#{@transform_lock_id};),
|
74
|
+
@logger
|
75
|
+
).result
|
76
|
+
|
77
|
+
# table is unlocked
|
78
|
+
if r == 0
|
79
|
+
l = JDBCHelpers::Execute.new(
|
80
|
+
jdbc_connection,
|
81
|
+
%{insert into #{@transform_lock_table}
|
82
|
+
(
|
83
|
+
transform_lock_id,
|
84
|
+
transform_name,
|
85
|
+
transform_lock_time
|
86
|
+
)
|
87
|
+
values
|
88
|
+
(
|
89
|
+
#{@transform_lock_id},
|
90
|
+
'#{@transform_name}',
|
91
|
+
current_timestamp
|
92
|
+
);},
|
93
|
+
@logger
|
94
|
+
)
|
95
|
+
|
96
|
+
if @database_manager.supports_transactions == true
|
97
|
+
l = JDBCHelpers::Execute.new(
|
98
|
+
jdbc_connection,
|
99
|
+
@database_manager.commit_statement,
|
100
|
+
@logger
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
lock_value = true
|
105
|
+
else
|
106
|
+
if @database_manager.supports_transactions == true
|
107
|
+
l = JDBCHelpers::Execute.new(
|
108
|
+
jdbc_connection,
|
109
|
+
@database_manager.rollback_statement,
|
110
|
+
@logger
|
111
|
+
)
|
112
|
+
end
|
113
|
+
lock_value = false
|
114
|
+
end
|
115
|
+
|
116
|
+
if lock_value == true
|
117
|
+
@logger.info("#{@transform_name} transform lock acquired;")
|
118
|
+
else
|
119
|
+
@logger.info("#{@transform_name} is locked by another process")
|
120
|
+
end
|
121
|
+
ensure
|
122
|
+
# close this connection
|
123
|
+
jdbc_connection.close
|
124
|
+
|
125
|
+
lock_value
|
126
|
+
end
|
127
|
+
|
128
|
+
# Removes transform lock if it's present.
|
129
|
+
def remove_lock
|
130
|
+
# get a fresh jdbc connection...
|
131
|
+
# to avoid committing the main transform unnecessarily
|
132
|
+
jdbc_connection = create_jdbc_connection
|
133
|
+
|
134
|
+
if @database_manager.supports_transactions == true
|
135
|
+
l = JDBCHelpers::Execute.new(
|
136
|
+
jdbc_connection,
|
137
|
+
@database_manager.begin_statement,
|
138
|
+
@logger
|
139
|
+
)
|
140
|
+
end
|
141
|
+
|
142
|
+
if @database_manager.supports_table_locking == true
|
143
|
+
l = JDBCHelpers::Execute.new(
|
144
|
+
jdbc_connection,
|
145
|
+
@database_manager.lock_table_statement(@transform_lock_table),
|
146
|
+
@logger
|
147
|
+
)
|
148
|
+
end
|
149
|
+
|
150
|
+
# delete the lock record and commit
|
151
|
+
JDBCHelpers::Execute.new(
|
152
|
+
jdbc_connection,
|
153
|
+
%(delete from #{@transform_lock_table}
|
154
|
+
where transform_name='#{@transform_name}'
|
155
|
+
and transform_lock_id=#{@transform_lock_id};),
|
156
|
+
@logger
|
157
|
+
)
|
158
|
+
|
159
|
+
c = JDBCHelpers::Execute.new(
|
160
|
+
jdbc_connection,
|
161
|
+
@database_manager.commit_statement,
|
162
|
+
@logger
|
163
|
+
)
|
164
|
+
ensure
|
165
|
+
jdbc_connection.close
|
166
|
+
|
167
|
+
return true
|
168
|
+
end
|
169
|
+
|
170
|
+
# TBD
|
171
|
+
def lock_all_available_transforms
|
172
|
+
# undefined at this time
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Sneaql
|
2
|
+
module Core
|
3
|
+
# Parses a step file into discrete statements.
|
4
|
+
# Also performs validation of all Sneaql tags.
|
5
|
+
class StepParser
|
6
|
+
# array of raw statement text
|
7
|
+
attr_reader :statements
|
8
|
+
attr_reader :expression_handler
|
9
|
+
|
10
|
+
# @param [String] file_path pathname to step file
|
11
|
+
# @param [Sneaql::ExpressionHandler] expression_handler
|
12
|
+
# @param [Sneaql::RecordsetManager] recordset_manager
|
13
|
+
# @param [Logger] logger optional logger, if omitted default logger will be used
|
14
|
+
def initialize(file_path, expression_handler, recordset_manager, logger = nil)
|
15
|
+
@logger = logger ? logger : Logger.new(STDOUT)
|
16
|
+
@expression_handler = expression_handler
|
17
|
+
@recordset_manager = recordset_manager
|
18
|
+
|
19
|
+
# parse the statements from the file and store them in an array
|
20
|
+
# this is a simple text parsing based upon the /*- delimiter
|
21
|
+
@statements = parse_statements_from_file(file_path)
|
22
|
+
|
23
|
+
raise Sneaql::Exceptions::NoStatementsFoundInFile if @statements == []
|
24
|
+
end
|
25
|
+
|
26
|
+
# Performs the actual parsing from file
|
27
|
+
# @param [String] file_path
|
28
|
+
def parse_statements_from_file(file_path)
|
29
|
+
@logger.info("parsing statements from step file #{file_path}")
|
30
|
+
stmt = []
|
31
|
+
File.read(file_path).split('/*-').each { |s| stmt << "/*-#{s.strip}" }
|
32
|
+
# delete the first element because of the way it splits
|
33
|
+
stmt.delete_at(0)
|
34
|
+
@logger.info("#{stmt.length} statements found")
|
35
|
+
stmt
|
36
|
+
rescue => e
|
37
|
+
@logger.error("file parsing error :#{e.message}")
|
38
|
+
e.backtrace.each { |b| @logger.error b.to_s }
|
39
|
+
raise Sneaql::Exceptions::StatementParsingError
|
40
|
+
end
|
41
|
+
|
42
|
+
# Extracts tag and splits into an array
|
43
|
+
# @param [String] statement_text_with_command
|
44
|
+
# @return [Array]
|
45
|
+
def tag_splitter(statement_text_with_command)
|
46
|
+
# splits out all the tag elements into an array
|
47
|
+
statement_text_with_command.split('-*/')[0].gsub('/*-', '').strip.split
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns command tag from statement at specified index. Allows for
|
51
|
+
# @param [Fixnum] indx index of statement in statements array
|
52
|
+
# @return [Hash]
|
53
|
+
def command_at_index(indx)
|
54
|
+
parsed_tag = tag_splitter(@statements[indx])
|
55
|
+
{ command: parsed_tag[0], arguments: parsed_tag[1..parsed_tag.length - 1] }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Validates the Sneaql command tag and arguments
|
59
|
+
# @return [Boolean]
|
60
|
+
def valid_arguments_in_all_statements?
|
61
|
+
all_statements_valid = true
|
62
|
+
@statements.each_with_index do |_s, i|
|
63
|
+
cmd = command_at_index(i)
|
64
|
+
@logger.debug("validating #{cmd}")
|
65
|
+
unless statement_args_are_valid?(cmd)
|
66
|
+
all_statements_valid = false
|
67
|
+
@logger.info "argument validation error: #{cmd}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
return all_statements_valid
|
71
|
+
end
|
72
|
+
|
73
|
+
# Checks to see if the arguments for a given command are valid.
|
74
|
+
# This is done by calling the validate_args method of the command class.
|
75
|
+
# @param [Hash] this_cmd parsed command tag
|
76
|
+
# @return [Boolean]
|
77
|
+
def statement_args_are_valid?(this_cmd)
|
78
|
+
c = Sneaql::Core.find_class(:command, this_cmd[:command]).new(
|
79
|
+
nil,
|
80
|
+
@expression_handler,
|
81
|
+
@recordset_manager,
|
82
|
+
nil,
|
83
|
+
@logger
|
84
|
+
)
|
85
|
+
c.validate_args(this_cmd[:arguments])
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Sneaql
|
4
|
+
module Core
|
5
|
+
#manages stored recordsets in sneaql transforms
|
6
|
+
class RecordsetManager
|
7
|
+
attr_reader :recordset
|
8
|
+
|
9
|
+
def initialize(expression_manager, logger = nil)
|
10
|
+
@logger = logger ? logger : Logger.new(STDOUT)
|
11
|
+
@expression_manager = expression_manager
|
12
|
+
@recordset = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
# Stores a recordset if it is in a valid format.
|
16
|
+
# @param [String] name name for recordset
|
17
|
+
# @param [Array<Hash>] rs recordset to store
|
18
|
+
def store_recordset(name, rs)
|
19
|
+
raise Sneaql::RecordsetIsNotAnArray unless rs.class == Array
|
20
|
+
raise Sneaql::RecordsetContainsInconsistentOrInvalidTypes unless recordset_valid?(rs)
|
21
|
+
recordset[name] = rs
|
22
|
+
end
|
23
|
+
|
24
|
+
# Validates recordset. Must be an array of hashes with identical keys.
|
25
|
+
# @param [Array<Hash>] rs recordset to validate
|
26
|
+
def recordset_valid?(rs)
|
27
|
+
return false unless rs.class == Array
|
28
|
+
r1 = rs[0].keys
|
29
|
+
|
30
|
+
rs.each do |record|
|
31
|
+
return false unless record.class == Hash
|
32
|
+
return false unless r1 == record.keys
|
33
|
+
record.keys {|k| puts k; return false unless valid_element_data_types.include?(record[k].class)}
|
34
|
+
end
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
# Ruby data types that are valid as recordset fields.
|
39
|
+
# @return [Array<Class>]
|
40
|
+
def valid_element_data_types
|
41
|
+
[Fixnum, String, Float]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Validates that the string will make a valid recordset name.
|
45
|
+
# @param [String] name
|
46
|
+
# @return [Boolean]
|
47
|
+
def valid_recordset_name?(name)
|
48
|
+
return false unless name.match(/^\w+/)
|
49
|
+
h = {}
|
50
|
+
h[name] == 1
|
51
|
+
rescue
|
52
|
+
return false
|
53
|
+
else
|
54
|
+
return true
|
55
|
+
end
|
56
|
+
|
57
|
+
# Validates that the recordset name doesn't conflict with session var names
|
58
|
+
# @param [String] name
|
59
|
+
# @return [Boolean]
|
60
|
+
def recordset_name_conflicts_with_variables?(name)
|
61
|
+
@expression_manager.session_variables.key?(name)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Parses a recordset expression.
|
65
|
+
# @param [Array<Hash>] args
|
66
|
+
# @return [Array<Hash>]
|
67
|
+
def parse_recordset_expression(args)
|
68
|
+
# takes in argument array as an argument
|
69
|
+
# returns array of expressions to be checked at run time
|
70
|
+
args.delete_at(0) # get rid of the first element, recordset ref
|
71
|
+
args.each_slice(4).to_a.map{ |x| { condition: x[0].downcase, field: x[1], operator: x[2], expression: x[3]}}
|
72
|
+
end
|
73
|
+
|
74
|
+
# applies a conditional expression set against a record.
|
75
|
+
# @param [Hash] record
|
76
|
+
# @param [Array<Hash>] expressions
|
77
|
+
# @return [Boolean]
|
78
|
+
def evaluate_expression_against_record(record, expressions)
|
79
|
+
conditions = []
|
80
|
+
expressions.each do |exp|
|
81
|
+
@logger.debug("applying #{exp} to #{record}")
|
82
|
+
raw_result = @expression_manager.compare_expressions(
|
83
|
+
exp[:operator],
|
84
|
+
record[exp[:field]],
|
85
|
+
exp[:expression]
|
86
|
+
)
|
87
|
+
if exp[:condition] == 'include'
|
88
|
+
conditions << raw_result
|
89
|
+
elsif exp[:condition] == 'exclude'
|
90
|
+
conditions << !raw_result
|
91
|
+
end
|
92
|
+
end
|
93
|
+
return !conditions.include?(false)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|