db_mod 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f148df036e59a1e23b2faa43b407b9493d54671a
4
+ data.tar.gz: c0ab239586891d8b91a5a82d79a17f4e19777f48
5
+ SHA512:
6
+ metadata.gz: 199028216fcaf755e20a67d607ad5f7ac3399c0c26ea3cee37f151dc5786c3e87b57245a9c4d42bbe5807bb6e3a1e6814e2ae9495ed15030e6d94c85f9071ea1
7
+ data.tar.gz: 7930c834d9705cfdb2e57556e9a478db09c0967ac16339d5728893561178431d09520fb98f7dc688ee7d331b683ffe8014ccfe9e25c7efaa7348427afe5d0158
data/.gitignore ADDED
@@ -0,0 +1,46 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+ .com.apple.timemachine.supported
4
+
5
+ ## TEXTMATE
6
+ *.tmproj
7
+ tmtags
8
+
9
+ ## EMACS
10
+ *~
11
+ \#*
12
+ .\#*
13
+
14
+ ## REDCAR
15
+ .redcar
16
+
17
+ ## VIM
18
+ *.swp
19
+ *.swo
20
+
21
+ ## RUBYMINE
22
+ .idea
23
+
24
+ ## PROJECT::GENERAL
25
+ coverage
26
+ doc
27
+ pkg
28
+ .rvmrc
29
+ .bundle
30
+ .yardoc/*
31
+ dist
32
+ Gemfile.lock
33
+ gemfiles/*.lock
34
+ tmp
35
+
36
+ ## Rubinius
37
+ .rbx
38
+
39
+ ## Bundler binstubs
40
+ bin
41
+
42
+ ## ripper-tags and gem-ctags
43
+ tags
44
+
45
+ ## PROJECT::SPECIFIC
46
+ .project
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format=documentation
data/.rubocop.yml ADDED
@@ -0,0 +1,5 @@
1
+ AllCops:
2
+ Exclude:
3
+ - vendor/**/*
4
+ - bin/**/*
5
+ - gemfiles/**/*
data/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2
4
+ - 2.1
5
+ - 2.0.0
6
+ - ruby-head
7
+ - 1.9.3
8
+
9
+ matrix:
10
+ allow_failures:
11
+ - rvm: ruby-head
12
+
13
+ gemfile:
14
+ - Gemfile
@@ -0,0 +1,22 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
6
+
7
+ Examples of unacceptable behavior by participants include:
8
+
9
+ * The use of sexualized language or imagery
10
+ * Personal attacks
11
+ * Trolling or insulting/derogatory comments
12
+ * Public or private harassment
13
+ * Publishing other's private information, such as physical or electronic addresses, without explicit permission
14
+ * Other unethical or unprofessional conduct.
15
+
16
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
17
+
18
+ This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
19
+
20
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
21
+
22
+ This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem 'byebug' unless RUBY_VERSION < '2.0.0'
7
+ gem 'rake'
8
+ gem 'redcarpet'
9
+ gem 'rubocop'
10
+ gem 'guard'
11
+ gem 'guard-bundler'
12
+ gem 'guard-rspec'
13
+ gem 'guard-rubocop'
14
+ gem 'guard-yard'
15
+ end
data/Guardfile ADDED
@@ -0,0 +1,94 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ ## Uncomment and set this to only include directories you want to watch
5
+ # directories %w(app lib config test spec features) \
6
+ # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
7
+
8
+ ## Note: if you are using the `directories` clause above and you are not
9
+ ## watching the project directory ('.'), then you will want to move
10
+ ## the Guardfile to a watched dir and symlink it back, e.g.
11
+ #
12
+ # $ mkdir config
13
+ # $ mv Guardfile config/
14
+ # $ ln -s config/Guardfile .
15
+ #
16
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
17
+
18
+ # Note: The cmd option is now required due to the increasing number of ways
19
+ # rspec may be run, below are examples of the most common uses.
20
+ # * bundler: 'bundle exec rspec'
21
+ # * bundler binstubs: 'bin/rspec'
22
+ # * spring: 'bin/rspec' (This will use spring if running and you have
23
+ # installed the spring binstubs per the docs)
24
+ # * zeus: 'zeus rspec' (requires the server to be started separately)
25
+ # * 'just' rspec: 'rspec'
26
+
27
+ guard :rspec, cmd: 'bundle exec rspec' do
28
+ require 'guard/rspec/dsl'
29
+ dsl = Guard::RSpec::Dsl.new(self)
30
+
31
+ # Feel free to open issues for suggestions and improvements
32
+
33
+ # RSpec files
34
+ rspec = dsl.rspec
35
+ watch(rspec.spec_helper) { rspec.spec_dir }
36
+ watch(rspec.spec_support) { rspec.spec_dir }
37
+ watch(rspec.spec_files)
38
+
39
+ # Ruby files
40
+ ruby = dsl.ruby
41
+ dsl.watch_spec_files_for(ruby.lib_files)
42
+
43
+ # Rails files
44
+ rails = dsl.rails(view_extensions: %w(erb haml slim))
45
+ dsl.watch_spec_files_for(rails.app_files)
46
+ dsl.watch_spec_files_for(rails.views)
47
+
48
+ watch(rails.controllers) do |m|
49
+ [
50
+ rspec.spec.call("routing/#{m[1]}_routing"),
51
+ rspec.spec.call("controllers/#{m[1]}_controller"),
52
+ rspec.spec.call("acceptance/#{m[1]}")
53
+ ]
54
+ end
55
+
56
+ # Rails config changes
57
+ watch(rails.spec_helper) { rspec.spec_dir }
58
+ watch(rails.routes) { "#{rspec.spec_dir}/routing" }
59
+ watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
60
+
61
+ # Capybara features specs
62
+ watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
63
+ watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
64
+
65
+ # Turnip features and steps
66
+ watch(%r{^spec/acceptance/(.+)\.feature$})
67
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
68
+ Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance'
69
+ end
70
+ end
71
+
72
+ guard :rubocop do
73
+ watch(/.+\.rb$/)
74
+ watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
75
+ end
76
+
77
+ guard 'yard' do
78
+ watch(%r{app/.+\.rb})
79
+ watch(%r{lib/.+\.rb})
80
+ watch(%r{ext/.+\.c})
81
+ watch(/.+\.md$/)
82
+ end
83
+
84
+ guard :bundler do
85
+ require 'guard/bundler'
86
+ require 'guard/bundler/verify'
87
+ helper = Guard::Bundler::Verify.new
88
+
89
+ files = ['Gemfile']
90
+ files += Dir['*.gemspec'] if files.any? { |f| helper.uses_gemspec?(f) }
91
+
92
+ # Assume files are symlinked from somewhere
93
+ files.each { |file| watch(helper.real_path(file)) }
94
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010-2015 Douglas Stephen Lake-Hammond
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,170 @@
1
+ ## `db_mod`
2
+
3
+ Database enabled modules for ruby.
4
+
5
+ ## Description
6
+
7
+ The `db_mod` gem is a simple framework that helps you organise your
8
+ database access functions into modular libraries that can be included
9
+ in your projects to give them selective access to facets of your data.
10
+
11
+ ## Usage
12
+
13
+ ### The database connection
14
+
15
+ At its most basic, `db_mod` provides `db_connect`, `query`, and
16
+ `transaction`:
17
+
18
+ ```ruby
19
+ require 'db_mod'
20
+
21
+ module MyFunctions
22
+ include DbMod
23
+
24
+ def get_stuff
25
+ query 'SELECT * FROM stuff'
26
+ end
27
+
28
+ def do_complicated_thing(input)
29
+ transaction do # calls BEGIN
30
+ query 'INSERT ...'
31
+ query 'UPDATE ...'
32
+ query 'DELETE ...'
33
+
34
+ output = query 'SELECT ...'
35
+ fail if output.empty? # calls ROLLBACK
36
+ end # calls COMMIT
37
+ end
38
+ end
39
+
40
+ include MyFunctions
41
+
42
+ db_connect(
43
+ db: 'mydb',
44
+ host: 'localhost', # defaults to local socket
45
+ port: 5432, # this is the default
46
+ user: 'myuser', # default is ENV['USER']
47
+ pass: 'password' # attempts trusted connection by default
48
+ )
49
+
50
+ get_stuff.each do |thing|
51
+ thing['id'] # => '1'
52
+ # ...
53
+ end
54
+ ```
55
+
56
+ #### `create`: Module instances
57
+
58
+ Each module also comes with its own `create` function,
59
+ which instantiates an object exposing all of the module's functions.
60
+
61
+ ```ruby
62
+ # Standard connection options can be used.
63
+ # db_connect will be called.
64
+ db = MyFunctions.create db: 'mydb'
65
+
66
+ # Or an existing connection object can be passed
67
+ db = MyFunctions.create PGconn.connect # ...
68
+
69
+ db.get_stuff
70
+ ```
71
+
72
+ #### `@conn`: The connection object
73
+
74
+ The connection created by `db_connect` or `create` will be stored
75
+ in the instance variable `@conn`. This instance variable may be
76
+ set explicitly instead of calling `db_connect`, allowing arbitrary
77
+ sharing of database connections between modules and objects.
78
+ See notes below on using this technique with `def_prepared`.
79
+
80
+ ### Module heirarchies
81
+
82
+ By including multiple modules into the same class or object, they
83
+ will all use the same connection object supplied by `db_connect`.
84
+ This connection object is stored in the instance variable `@conn`
85
+ and can be supplied manually:
86
+
87
+ ```ruby
88
+ module DbAccess
89
+ # includes DbMod and defines do_things
90
+ include Db::Things
91
+
92
+ # includes DbMod and defines do_stuff
93
+ include Db::Stuff
94
+
95
+ def things_n_stuff
96
+ transaction do
97
+ do_things
98
+ do_stuff
99
+ end
100
+ end
101
+ end
102
+
103
+ db = DbAccess.create db: 'mydb'
104
+ db.things_n_stuff
105
+ ```
106
+
107
+ ### `def_prepared`: Declaring SQL statements
108
+
109
+ Modules which include `DbMod` can declare prepared statements
110
+ using the module function `def_prepared`. These statements will
111
+ be prepared on the connection when `db_connect` is called.
112
+ A method will be defined in the module with the same name as
113
+ the prepared statement, provided as a convenience for executing
114
+ the statement:
115
+
116
+ ```ruby
117
+ module Db
118
+ module Things
119
+ # Statements can use named parameters:
120
+ def_prepared :foo, <<-SQL
121
+ SELECT *
122
+ FROM foo
123
+ WHERE id = $id
124
+ AND b > $minimum_value
125
+ AND c > $minimum_value
126
+ SQL
127
+ end
128
+
129
+ module Stuff
130
+ # Indexed parameters also work:
131
+ def_prepared :bar, <<-SQL
132
+ INSERT INTO bar
133
+ (a, b, c)
134
+ VALUES
135
+ ($1, $2, $1)
136
+ SQL
137
+ end
138
+
139
+ module ComplicatedStuff
140
+ # Statements on included modules will also
141
+ # be executed when db_connect is called.
142
+ include Things
143
+ include Stuff
144
+
145
+ def complicated_thing!(id, min)
146
+ transaction do
147
+ foo(id: id, minimum_value: min).each do |thing|
148
+ bar(thing['a'], thing['b'])
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ include Db::ComplicatedStuff
156
+ db_connect db: 'mydb'
157
+
158
+ complicated_thing!(1, 2)
159
+ ```
160
+
161
+ Note that if an existing connection is supplied to `create` or `@conn`
162
+ then declared statements will not be automatically prepared. In this
163
+ case the module function `prepare_all_statements(conn)` can be used
164
+ to prepare all statements declared in the module or any included
165
+ modules on the given connection.
166
+
167
+ ```ruby
168
+ db = Db::ComplicatedStuff.create my_conn
169
+ Db::ComplicatedStuff.prepare_all_statements my_conn
170
+ ```
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup :default, :test, :development
4
+
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ require 'rspec/core/rake_task'
8
+ RSpec::Core::RakeTask.new(:spec) do |spec|
9
+ spec.pattern = 'spec/**/*_spec.rb'
10
+ end
11
+
12
+ task :spec
13
+
14
+ require 'rainbow/ext/string' unless String.respond_to?(:color)
15
+ require 'rubocop/rake_task'
16
+ RuboCop::RakeTask.new
17
+
18
+ task default: [:rubocop, :spec]
19
+
20
+ require 'yard'
21
+ DOC_FILES = ['lib/**/*.rb', 'README.md']
22
+
23
+ YARD::Rake::YardocTask.new(:doc) do |t|
24
+ t.files = DOC_FILES
25
+ end
data/db_mod.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+ require 'db_mod/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'db_mod'
6
+ s.version = DbMod::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ['Doug Hammond']
9
+ s.email = ['d.lakehammond@gmail.com']
10
+ s.summary = 'Ruby framework for building modular db-access libs.'
11
+ s.description = 'Organize your database-intensive batch scripts with db_mod.'
12
+ s.license = 'MIT'
13
+
14
+ s.add_runtime_dependency 'pg'
15
+ s.add_runtime_dependency 'docile'
16
+
17
+ s.add_development_dependency 'simplecov'
18
+ s.add_development_dependency 'rspec'
19
+ s.add_development_dependency 'rspec-mocks'
20
+ s.add_development_dependency 'yard'
21
+ s.add_development_dependency 'bundler'
22
+
23
+ s.files = `git ls-files`.split("\n")
24
+ s.test_files = `git ls-files -- spec/*`.split("\n")
25
+ s.require_paths = %w(lib)
26
+ end
@@ -0,0 +1,51 @@
1
+ module DbMod
2
+ # Provides the +create+ function which
3
+ # is added to all modules which include {DbMod}.
4
+ # This function creates an object which exposes
5
+ # the functions defined in the module, allowing
6
+ # them to be used without namespace pollution.
7
+ #
8
+ # The function may be used in two forms. It may
9
+ # be called with an options hash, in which case
10
+ # {DbMod#db_connect} will be used to create a
11
+ # new connection object. Alternatively an
12
+ # existing connection object may be passed,
13
+ # which will be used for all database queries.
14
+ module Create
15
+ # Defines a module-specific +create+ function
16
+ # for a module that has just had {DbMod}
17
+ # included.
18
+ #
19
+ # @param mod [Module]
20
+ def self.setup(mod)
21
+ mod.class.instance_eval do
22
+ define_method(:create) do |options = {}|
23
+ @instantiable_class ||= Create.instantiable_class(self)
24
+
25
+ @instantiable_class.new(options)
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ # Creates a class which inherits from the given module
33
+ # and can be instantiated with either a connection object
34
+ # or some connection options.
35
+ #
36
+ # @param mod [Module]
37
+ def self.instantiable_class(mod)
38
+ Class.new do
39
+ include mod
40
+
41
+ define_method(:initialize) do |options|
42
+ if options.is_a? PGconn
43
+ self.conn = options
44
+ else
45
+ db_connect options
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'base'
2
+
3
+ module DbMod
4
+ module Exceptions
5
+ # Raised when an attempt has been made to
6
+ # start a transaction on a connection that
7
+ # already has one open.
8
+ class AlreadyInTransaction < Base
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module DbMod
2
+ module Exceptions
3
+ # Base class for db_mod exceptions
4
+ class Base < StandardError
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'base'
2
+
3
+ module DbMod
4
+ module Exceptions
5
+ # Raised when an attempt has been made to
6
+ # access db_mod functionality without first
7
+ # creating or supplying a connection object.
8
+ class ConnectionNotSet < Base
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'base'
2
+
3
+ module DbMod
4
+ module Exceptions
5
+ # Raised when an attempt has been made to
6
+ # define or prepare statements for a module that includes
7
+ # more than one statement with the same name.
8
+ class DuplicateStatementName < Base
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'exceptions/connection_not_set'
2
+ require_relative 'exceptions/already_in_transaction'
3
+ require_relative 'exceptions/duplicate_statement_name'
4
+
5
+ module DbMod
6
+ # Non-standard errors raised by db_mod
7
+ module Exceptions
8
+ end
9
+ end
@@ -0,0 +1,108 @@
1
+ module DbMod
2
+ module Statements
3
+ # Parsing and validation of query parameters
4
+ # for prepared SQL statements
5
+ module Params
6
+ # Assert that the named arguments given for the prepared statement
7
+ # with the given name satisfy expectations.
8
+ #
9
+ # @param expected [Array<Symbol>] the parameters expected to be present
10
+ # @param args [Hash] given parameters
11
+ # @return [Array] values to be passed to the prepared statement
12
+ def self.valid_named_args!(expected, args)
13
+ unless args.is_a? Hash
14
+ fail ArgumentError, "invalid argument: #{args.inspect}"
15
+ end
16
+
17
+ if args.size != expected.size
18
+ fail ArgumentError, "#{args.size} args given, #{expected.size} needed"
19
+ end
20
+
21
+ expected.map do |arg|
22
+ args[arg] || fail(ArgumentError, "missing arg #{arg}")
23
+ end
24
+ end
25
+
26
+ # Regex matching a numbered parameter
27
+ NUMBERED_PARAM = /\$\d+/
28
+
29
+ # Regex matching a named parameter
30
+ NAMED_PARAM = /\$[a-z]+(?:_[a-z]+)*/
31
+
32
+ # For validation, named or numbered parameter
33
+ NAMED_OR_NUMBERED = /^\$(?:\d+|[a-z]+(?:_[a-z]+)*)$/
34
+
35
+ # Parses parameters, named or numbered, from an SQL
36
+ # statement. See the {Prepared} module documentation
37
+ # for more. This method may modify the sql statement
38
+ # to change named parameters to numbered parameters.
39
+ # If the query uses numbered parameters, an integer
40
+ # will be returned that is the arity of the statement.
41
+ # If the query uses named parameters, an array of
42
+ # symbols will be returned, giving the order in which
43
+ # the named parameters should be fed into the
44
+ # statement.
45
+ #
46
+ # @param sql [String] statement to prepare
47
+ # @return [Fixnum,Array<Symbol>] description of
48
+ # prepared statement's parameters
49
+ def self.parse_params!(sql)
50
+ Params.valid_sql_params! sql
51
+ numbered = sql.scan NUMBERED_PARAM
52
+ named = sql.scan NAMED_PARAM
53
+
54
+ if numbered.any?
55
+ fail ArgumentError, 'mixed named and numbered params' if named.any?
56
+ Params.parse_numbered_params! numbered
57
+ else
58
+ Params.parse_named_params! sql, named
59
+ end
60
+ end
61
+
62
+ # Fails if any parameters in an sql query aren't
63
+ # in the expected format. They must either be
64
+ # lower_case_a_to_z or digits only.
65
+ def self.valid_sql_params!(sql)
66
+ sql.scan(/\$\S+/) do |param|
67
+ unless param =~ NAMED_OR_NUMBERED
68
+ fail ArgumentError, "Invalid parameter #{param}"
69
+ end
70
+ end
71
+ end
72
+
73
+ # Validates the numbered parameters given (i.e. no gaps),
74
+ # and returns the parameter count.
75
+ #
76
+ # @param params [Array<String>] '$1','$2', etc...
77
+ # @return [Fixnum] parameter count
78
+ def self.parse_numbered_params!(params)
79
+ params.sort!
80
+ params.uniq!
81
+ if params.last[1..-1].to_i != params.length ||
82
+ params.first[1..-1].to_i != 1
83
+ fail ArgumentError, 'Invalid parameter list'
84
+ end
85
+
86
+ params.length
87
+ end
88
+
89
+ # Replaces the given list of named parameters in the
90
+ # query string with numbered parameters, and returns
91
+ # an array of symbols giving the order the parameters
92
+ # should be fed into the prepared statement for execution.
93
+ #
94
+ # @param sql [String] the SQL statement. Will be modified.
95
+ # @param params [Array<String>] '$one', '$two', etc...
96
+ # @return [Array<Symbol>] unique list of named parameters
97
+ def self.parse_named_params!(sql, params)
98
+ unique_params = params.uniq
99
+ params.each do |param|
100
+ index = unique_params.index(param)
101
+ sql[param] = "$#{index + 1}"
102
+ end
103
+
104
+ unique_params.map { |p| p[1..-1].to_sym }
105
+ end
106
+ end
107
+ end
108
+ end