sneaql 0.0.8-java
Sign up to get free protection for your applications and to get access to all the features.
- 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
|