Skip to content

Commit c7c91c4

Browse files
author
Guy Boertje
authored
Add Prepared Statement support (#349)
* Move code around so backtrace shows which class is being initialised. * Add prepared statements, preserve previous behavoiur. * DOH remnant @sql_last_value * Add prep statement code, tests, docs and bump version. * Update lib/logstash/plugin_mixins/jdbc/statement_handler.rb Co-Authored-By: Colin Surprenant <[email protected]> * Update lib/logstash/plugin_mixins/jdbc/value_tracking.rb Co-Authored-By: Rob Bavey <[email protected]> * updates per PR review, non count logging, stricter validation Fixes #166 Fixes #233
1 parent b1076ca commit c7c91c4

File tree

9 files changed

+389
-67
lines changed

9 files changed

+389
-67
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 4.3.16
2+
- Add support for prepared statements [Issue 233](https://github.com/logstash-plugins/logstash-input-jdbc/issues/233)
3+
14
## 4.3.15
25
- Use atomic booleam to load drivers once
36
- Added CHANGELOG entries

docs/index.asciidoc

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,35 @@ input {
140140
}
141141
---------------------------------------------------------------------------------------------------
142142

143+
==== Prepared Statements
144+
145+
Using server side prepared statements can speed up execution times as the server optimises the query plan and execution.
146+
147+
NOTE: Not all JDBC accessible technologies will support prepared statements.
148+
149+
With the introduction of Prepared Statement support comes a different code execution path and some new settings. Most of the existing settings are still useful but there are several new settings for Prepared Statements to read up on.
150+
Use the boolean setting `use_prepared_statements` to enable this execution mode. Use the `prepared_statement_name` setting to specify a name for the Prepared Statement, this identifies the prepared statement locally and remotely and it should be unique in your config and on the database. Use the `prepared_statement_bind_values` array setting to specify the bind values, use the exact string `:sql_last_value` (multiple times if necessary) for the predefined parameter mentioned before. The `statement` (or `statement_path`) setting still holds the SQL statement but to use bind variables you must use the `?` character as a placeholder in the exact order found in the `prepared_statement_bind_values` array.
151+
152+
NOTE: Building count queries around a prepared statement is not supported at this time and because jdbc paging uses count queries under the hood, jdbc paging is not supported with prepared statements at this time either. Therefore, `jdbc_paging_enabled`, `jdbc_page_size` settings are ignored when using prepared statements.
153+
154+
Example:
155+
[source,ruby]
156+
---------------------------------------------------------------------------------------------------
157+
input {
158+
jdbc {
159+
statement => "SELECT * FROM mgd.seq_sequence WHERE _sequence_key > ? AND _sequence_key < ? + ? ORDER BY _sequence_key ASC"
160+
prepared_statement_bind_values => [":sql_last_value", ":sql_last_value", 4]
161+
prepared_statement_name => "foobar"
162+
use_prepared_statements => true
163+
use_column_value => true
164+
tracking_column_type => "numeric"
165+
tracking_column => "_sequence_key"
166+
last_run_metadata_path => "/elastic/tmp/testing/confs/test-jdbc-int-sql_last_value.yml"
167+
# ... other configuration bits
168+
}
169+
}
170+
---------------------------------------------------------------------------------------------------
171+
143172

144173
[id="plugins-{type}s-{plugin}-options"]
145174
==== Jdbc Input Configuration Options
@@ -149,7 +178,6 @@ This plugin supports the following configuration options plus the <<plugins-{typ
149178
[cols="<,<,<",options="header",]
150179
|=======================================================================
151180
|Setting |Input type|Required
152-
| <<plugins-{type}s-{plugin}-plugin_timezone>> |<<string,string>>, one of `["local", "utc"]`|No
153181
| <<plugins-{type}s-{plugin}-clean_run>> |<<boolean,boolean>>|No
154182
| <<plugins-{type}s-{plugin}-columns_charset>> |<<hash,hash>>|No
155183
| <<plugins-{type}s-{plugin}-connection_retry_attempts>> |<<number,number>>|No
@@ -170,6 +198,9 @@ This plugin supports the following configuration options plus the <<plugins-{typ
170198
| <<plugins-{type}s-{plugin}-last_run_metadata_path>> |<<string,string>>|No
171199
| <<plugins-{type}s-{plugin}-lowercase_column_names>> |<<boolean,boolean>>|No
172200
| <<plugins-{type}s-{plugin}-parameters>> |<<hash,hash>>|No
201+
| <<plugins-{type}s-{plugin}-plugin_timezone>> |<<string,string>>, one of `["local", "utc"]`|No
202+
| <<plugins-{type}s-{plugin}-prepared_statement_bind_values>> |<<array,array>>|No
203+
| <<plugins-{type}s-{plugin}-prepared_statement_name>> |<<string,string>>|No
173204
| <<plugins-{type}s-{plugin}-record_last_run>> |<<boolean,boolean>>|No
174205
| <<plugins-{type}s-{plugin}-schedule>> |<<string,string>>|No
175206
| <<plugins-{type}s-{plugin}-sequel_opts>> |<<hash,hash>>|No
@@ -179,6 +210,7 @@ This plugin supports the following configuration options plus the <<plugins-{typ
179210
| <<plugins-{type}s-{plugin}-tracking_column>> |<<string,string>>|No
180211
| <<plugins-{type}s-{plugin}-tracking_column_type>> |<<string,string>>, one of `["numeric", "timestamp"]`|No
181212
| <<plugins-{type}s-{plugin}-use_column_value>> |<<boolean,boolean>>|No
213+
| <<plugins-{type}s-{plugin}-use_prepared_statements>> |<<boolean,boolean>>|No
182214
|=======================================================================
183215

184216
Also see <<plugins-{type}s-{plugin}-common-options>> for a list of options supported by all
@@ -409,6 +441,22 @@ Whether to force the lowercasing of identifier fields
409441

410442
Hash of query parameter, for example `{ "target_id" => "321" }`
411443

444+
[id="plugins-{type}s-{plugin}-prepared_statement_bind_values"]
445+
===== `prepared_statement_bind_values`
446+
447+
* Value type is <<array,array>>
448+
* Default value is `[]`
449+
450+
Array of bind values for the prepared statement. `:sql_last_value` is a reserved predefined string
451+
452+
[id="plugins-{type}s-{plugin}-prepared_statement_name"]
453+
===== `prepared_statement_name`
454+
455+
* Value type is <<string,string>>
456+
* Default value is `""`
457+
458+
Name given to the prepared statement. It must be unique in your config and in the database
459+
412460
[id="plugins-{type}s-{plugin}-record_last_run"]
413461
===== `record_last_run`
414462

@@ -506,6 +554,13 @@ When set to `true`, uses the defined
506554
<<plugins-{type}s-{plugin}-tracking_column>> value as the `:sql_last_value`. When set
507555
to `false`, `:sql_last_value` reflects the last time the query was executed.
508556

557+
[id="plugins-{type}s-{plugin}-use_prepared_statements"]
558+
===== `use_prepared_statements`
559+
560+
* Value type is <<boolean,boolean>>
561+
* Default value is `false`
562+
563+
When set to `true`, enables prepare statement usage
509564

510565
[id="plugins-{type}s-{plugin}-common-options"]
511566
include::{include_path}/{type}.asciidoc[]

lib/logstash/inputs/jdbc.rb

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,12 @@ module LogStash module Inputs class Jdbc < LogStash::Inputs::Base
201201
# this will only convert column0 that has ISO-8859-1 as an original encoding.
202202
config :columns_charset, :validate => :hash, :default => {}
203203

204+
config :use_prepared_statements, :validate => :boolean, :default => false
205+
206+
config :prepared_statement_name, :validate => :string, :default => ""
207+
208+
config :prepared_statement_bind_values, :validate => :array, :default => []
209+
204210
attr_reader :database # for test mocking/stubbing
205211

206212
public
@@ -217,17 +223,25 @@ def register
217223
end
218224
end
219225

220-
set_value_tracker(LogStash::PluginMixins::Jdbc::ValueTracking.build_last_value_tracker(self))
221-
set_statement_logger(LogStash::PluginMixins::Jdbc::CheckedCountLogger.new(@logger))
222-
223-
@enable_encoding = !@charset.nil? || !@columns_charset.empty?
224-
225226
unless @statement.nil? ^ @statement_filepath.nil?
226227
raise(LogStash::ConfigurationError, "Must set either :statement or :statement_filepath. Only one may be set at a time.")
227228
end
228229

229230
@statement = ::File.read(@statement_filepath) if @statement_filepath
230231

232+
# must validate prepared statement mode after trying to read in from @statement_filepath
233+
if @use_prepared_statements
234+
validation_errors = validate_prepared_statement_mode
235+
unless validation_errors.empty?
236+
raise(LogStash::ConfigurationError, "Prepared Statement Mode validation errors: " + validation_errors.join(", "))
237+
end
238+
end
239+
240+
set_value_tracker(LogStash::PluginMixins::Jdbc::ValueTracking.build_last_value_tracker(self))
241+
set_statement_logger(LogStash::PluginMixins::Jdbc::CheckedCountLogger.new(@logger))
242+
243+
@enable_encoding = !@charset.nil? || !@columns_charset.empty?
244+
231245
if (@jdbc_password_filepath and @jdbc_password)
232246
raise(LogStash::ConfigurationError, "Only one of :jdbc_password, :jdbc_password_filepath may be set at a time.")
233247
end
@@ -248,7 +262,7 @@ def register
248262

249263
# test injection points
250264
def set_statement_logger(instance)
251-
@statement_logger = instance
265+
@statement_handler = LogStash::PluginMixins::Jdbc::StatementHandler.build_statement_handler(self, instance)
252266
end
253267

254268
def set_value_tracker(instance)
@@ -275,10 +289,24 @@ def stop
275289

276290
private
277291

292+
def validate_prepared_statement_mode
293+
error_messages = []
294+
if @prepared_statement_name.empty?
295+
error_messages << "must provide a name for the Prepared Statement, it must be unique for the db session"
296+
end
297+
if @statement.count("?") != @prepared_statement_bind_values.size
298+
# mismatch in number of bind value elements to placeholder characters
299+
error_messages << "there is a mismatch between the number of statement `?` placeholders and :prepared_statement_bind_values array setting elements"
300+
end
301+
if @jdbc_paging_enabled
302+
# Pagination is not supported when using prepared statements
303+
error_messages << "JDBC pagination cannot be used at this time"
304+
end
305+
error_messages
306+
end
307+
278308
def execute_query(queue)
279-
# update default parameters
280-
@parameters['sql_last_value'] = @value_tracker.value
281-
execute_statement(@statement, @parameters) do |row|
309+
execute_statement do |row|
282310
if enable_encoding?
283311
## do the necessary conversions to string elements
284312
row = Hash[row.map { |k, v| [k.to_s, convert(k, v)] }]

lib/logstash/plugin_mixins/jdbc/checked_count_logger.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ def initialize(logger)
99
@in_debug = @logger.debug?
1010
end
1111

12-
def log_statement_parameters(query, statement, parameters)
12+
def disable_count
13+
@needs_check = false
14+
@count_is_supported = false
15+
end
16+
17+
def log_statement_parameters(statement, parameters, query)
1318
return unless @in_debug
14-
check_count_query(query) if @needs_check
19+
check_count_query(query) if @needs_check && query
1520
if @count_is_supported
1621
@logger.debug("Executing JDBC query", :statement => statement, :parameters => parameters, :count => execute_count(query))
1722
else
@@ -35,4 +40,4 @@ def execute_count(query)
3540
query.count
3641
end
3742
end
38-
end end end
43+
end end end

lib/logstash/plugin_mixins/jdbc/jdbc.rb

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require_relative "value_tracking"
77
require_relative "checked_count_logger"
88
require_relative "wrapped_driver"
9+
require_relative "statement_handler"
910

1011
java_import java.util.concurrent.locks.ReentrantLock
1112

@@ -246,18 +247,14 @@ def close_jdbc_connection
246247
end
247248

248249
public
249-
def execute_statement(statement, parameters)
250-
# sql_last_value has been set in params by caller
250+
def execute_statement
251251
success = false
252252
@connection_lock.lock
253253
open_jdbc_connection
254254
begin
255-
params = symbolized_params(parameters)
256-
query = @database[statement, params]
257255
sql_last_value = @use_column_value ? @value_tracker.value : Time.now.utc
258256
@tracking_column_warning_sent = false
259-
@statement_logger.log_statement_parameters(query, statement, params)
260-
perform_query(query) do |row|
257+
@statement_handler.perform_query(@database, @value_tracker.value) do |row|
261258
sql_last_value = get_column_value(row) if @use_column_value
262259
yield extract_values_from(row)
263260
end
@@ -273,55 +270,23 @@ def execute_statement(statement, parameters)
273270
return success
274271
end
275272

276-
# Performs the query, respecting our pagination settings, yielding once per row of data
277-
# @param query [Sequel::Dataset]
278-
# @yieldparam row [Hash{Symbol=>Object}]
279-
private
280-
def perform_query(query)
281-
if @jdbc_paging_enabled
282-
query.each_page(@jdbc_page_size) do |paged_dataset|
283-
paged_dataset.each do |row|
284-
yield row
285-
end
286-
end
287-
else
288-
query.each do |row|
289-
yield row
290-
end
291-
end
292-
end
293-
294273
public
295274
def get_column_value(row)
296275
if !row.has_key?(@tracking_column.to_sym)
297276
if !@tracking_column_warning_sent
298277
@logger.warn("tracking_column not found in dataset.", :tracking_column => @tracking_column)
299278
@tracking_column_warning_sent = true
300279
end
301-
# If we can't find the tracking column, return the current value_tracker value
280+
# If we can't find the tracking column, return the current value in the ivar
302281
@value_tracker.value
303282
else
304283
# Otherwise send the updated tracking column
305284
row[@tracking_column.to_sym]
306285
end
307286
end
308287

309-
# Symbolize parameters keys to use with Sequel
310-
private
311-
def symbolized_params(parameters)
312-
parameters.inject({}) do |hash,(k,v)|
313-
case v
314-
when LogStash::Timestamp
315-
hash[k.to_sym] = v.time
316-
else
317-
hash[k.to_sym] = v
318-
end
319-
hash
320-
end
321-
end
322-
323288
private
324-
#Stringify row keys and decorate values when necessary
289+
#Stringify row keys and decorate values when necessary
325290
def extract_values_from(row)
326291
Hash[row.map { |k, v| [k.to_s, decorate_value(v)] }]
327292
end

0 commit comments

Comments
 (0)