stairwell 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +23 -2
- data/README.md +22 -9
- data/lib/stairwell.rb +21 -4
- data/lib/stairwell/bind_transformer.rb +11 -8
- data/lib/stairwell/query.rb +20 -3
- data/lib/stairwell/types/base_type.rb +17 -0
- data/lib/stairwell/types/boolean_type.rb +9 -0
- data/lib/stairwell/types/column_name_type.rb +13 -0
- data/lib/stairwell/types/date_time_type.rb +9 -0
- data/lib/stairwell/types/date_type.rb +9 -0
- data/lib/stairwell/types/float_type.rb +9 -0
- data/lib/stairwell/types/in_type.rb +28 -0
- data/lib/stairwell/types/integer_type.rb +9 -0
- data/lib/stairwell/types/null_type.rb +9 -0
- data/lib/stairwell/types/string_type.rb +9 -0
- data/lib/stairwell/types/table_name_type.rb +13 -0
- data/lib/stairwell/version.rb +1 -1
- data/stairwell.gemspec +7 -0
- data/test.db +0 -0
- metadata +43 -6
- data/lib/stairwell/core_extensions/core.rb +0 -77
- data/lib/stairwell/core_extensions/types.rb +0 -7
- data/lib/stairwell/type_validator.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9c4e331d0e1037b7e3a1e52cc0390bf84797e4898fa4d978e73a99454b81f37f
|
4
|
+
data.tar.gz: b58a61a030aa209e8b04511011a6dc1319d984d7347c5fdd23b581669a6220f2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc90cda2b768f201a94b04241b3f09bb555433e18ddfc8781ceb33f5a88ec641a1ad602f69f17c4da55b425757bfcb12115f95ec09c512ec999691f80ca688a0
|
7
|
+
data.tar.gz: 80c07a75ad1970ca578374a48757990edd1e1d19f37b97fb1bae8ceba3c236e629a7c4ba8ae5b03d44d4837835a49c2ce875851576d09d22f005c07f5a329e97
|
data/Gemfile.lock
CHANGED
@@ -1,18 +1,39 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
stairwell (0.1.
|
4
|
+
stairwell (0.1.2)
|
5
|
+
activerecord (>= 4.2.11)
|
6
|
+
sqlite3
|
5
7
|
|
6
8
|
GEM
|
7
9
|
remote: https://rubygems.org/
|
8
10
|
specs:
|
11
|
+
activemodel (6.0.3.4)
|
12
|
+
activesupport (= 6.0.3.4)
|
13
|
+
activerecord (6.0.3.4)
|
14
|
+
activemodel (= 6.0.3.4)
|
15
|
+
activesupport (= 6.0.3.4)
|
16
|
+
activesupport (6.0.3.4)
|
17
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
|
+
i18n (>= 0.7, < 2)
|
19
|
+
minitest (~> 5.1)
|
20
|
+
tzinfo (~> 1.1)
|
21
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
9
22
|
coderay (1.1.3)
|
23
|
+
concurrent-ruby (1.1.7)
|
24
|
+
i18n (1.8.5)
|
25
|
+
concurrent-ruby (~> 1.0)
|
10
26
|
method_source (1.0.0)
|
11
|
-
minitest (5.14.
|
27
|
+
minitest (5.14.2)
|
12
28
|
pry (0.13.1)
|
13
29
|
coderay (~> 1.1)
|
14
30
|
method_source (~> 1.0)
|
15
31
|
rake (12.3.3)
|
32
|
+
sqlite3 (1.4.2)
|
33
|
+
thread_safe (0.3.6)
|
34
|
+
tzinfo (1.2.7)
|
35
|
+
thread_safe (~> 0.1)
|
36
|
+
zeitwerk (2.4.0)
|
16
37
|
|
17
38
|
PLATFORMS
|
18
39
|
ruby
|
data/README.md
CHANGED
@@ -20,7 +20,7 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
-
Define a class in your app that inherits from `Stairwell::Query`. We're going to assume you are in a rails app, but this will work in any ruby app, ActiveRecord is
|
23
|
+
Define a class in your app that inherits from `Stairwell::Query`. We're going to assume you are in a rails app, but this will work in any ruby app, although ActiveRecord is a dependency of this gem.
|
24
24
|
In rails you could create a directory called `app/queries` for instance.
|
25
25
|
|
26
26
|
Define your `validate_type`, which will be the arguments you send in, and their type. For instance, if your query looks like this: `SELECT * FROM users WHERE name = :name`, and name is a `String`, your `validate_type` will look like this `validate_type :name, :string`, and you'll pass in a hash of your binds like this: `{ name: "<name value>" }`
|
@@ -34,6 +34,7 @@ class UsersSql < Stairwell::Query
|
|
34
34
|
validate_type :gpa, :float
|
35
35
|
validate_type :date_joined, :sql_date
|
36
36
|
validate_type :created_at, :sql_date_time
|
37
|
+
validate_type :favorite_numbers, [:integer]
|
37
38
|
|
38
39
|
query <<-SQL
|
39
40
|
SELECT
|
@@ -45,6 +46,7 @@ class UsersSql < Stairwell::Query
|
|
45
46
|
AND gpa = :gpa
|
46
47
|
AND date_joined = :date_joined
|
47
48
|
AND created_at >= :created_at
|
49
|
+
AND favorite_numbers IN (:favorite_numbers)
|
48
50
|
;
|
49
51
|
SQL
|
50
52
|
end
|
@@ -58,6 +60,7 @@ binds = {
|
|
58
60
|
gpa: 4.2
|
59
61
|
date_joined: "2008-08-28",
|
60
62
|
created_at: "2008-08-28 23:41:18",
|
63
|
+
favorite_numbers: [4, 7, 100]
|
61
64
|
}
|
62
65
|
|
63
66
|
# and call the following:
|
@@ -66,23 +69,33 @@ UsersSql.sql(binds)
|
|
66
69
|
|
67
70
|
# You will receive the following result:
|
68
71
|
|
69
|
-
"SELECT * FROM users WHERE name = 'First' age = 99 active = TRUE date_joined = '2008-08-28' created_at
|
72
|
+
"SELECT * FROM users WHERE name = 'First' AND age = 99 AND active = TRUE AND date_joined = '2008-08-28' AND created_at >= '2008-08-28 23:41:18' AND gpa = 4.2 AND favorite_numbers IN (4, 7, 100) ;"
|
70
73
|
```
|
71
74
|
|
72
75
|
Binds passed in are validated against the validate_type, so if you have a validate_type you must include that value in your binds hash.
|
73
76
|
They types of the binds are validated too.
|
74
77
|
The names binds in your sql are also validated.
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
+
All types are quoted using ActiveRecord quoting, which will be different depending on your database type (Mysql, postgres etc.)
|
79
|
+
|
80
|
+
## Supported Types
|
81
|
+
|
82
|
+
| Type | Values Accepted | Info |
|
83
|
+
|--------------|----------------------|------------------------------------------------------------------------------------------------------|
|
84
|
+
| :boolean | TrueClass/FalseClass | Not fully supported since many databases require 'IS TRUE' or 'IS FALSE' |
|
85
|
+
| :column_name | String | for quoting a column name |
|
86
|
+
| :date_time | String | only taking the actual string for now |
|
87
|
+
| :date | String | only taking the actual string for now |
|
88
|
+
| :float | Float | |
|
89
|
+
| [<type>] | Array | will quote any type provided in the array [:integer] |
|
90
|
+
| :integer | Integer | |
|
91
|
+
| :null | NilClass | nil/NULL values are not completely supported since many databases require 'IS NULL' or 'IS NOT NULL' |
|
92
|
+
| :string | String | |
|
93
|
+
| :table_name | String | for quoting a table name |
|
78
94
|
|
79
95
|
## Known issues
|
80
96
|
|
81
|
-
* nil/NULL values are not
|
97
|
+
* nil/NULL values are not completely supported, since many databases require `IS NULL`, or `IS NOT NULL`, you can use the null_type here, but it will only accept `nil`, and it will possibly not support what you're trying to do. YMMV.
|
82
98
|
* Date/Datetime are not validated for their format, it is expected that you will pass the correct format.
|
83
|
-
* Datetime in postgres is not currently working for equality, only for `>` or `<` or `>=` or `<=`
|
84
|
-
* Column/table quoting is not currently available.
|
85
|
-
* `IN` statements with arrays support is forthcoming.
|
86
99
|
|
87
100
|
|
88
101
|
## Development
|
data/lib/stairwell.rb
CHANGED
@@ -1,14 +1,31 @@
|
|
1
|
+
require "active_record"
|
1
2
|
require "date"
|
2
|
-
require "stairwell/bind_transformer"
|
3
3
|
require "stairwell/query"
|
4
|
-
require "stairwell/type_validator"
|
5
4
|
require "stairwell/version"
|
6
|
-
require "stairwell/core_extensions/core"
|
7
|
-
require "stairwell/core_extensions/types"
|
8
5
|
|
9
6
|
module Stairwell
|
10
7
|
class Error < StandardError; end
|
11
8
|
class InvalidBindType < StandardError; end
|
12
9
|
class InvalidBindCount < StandardError; end
|
13
10
|
class SqlBindMismatch < StandardError; end
|
11
|
+
|
12
|
+
TYPE_CLASSES = {
|
13
|
+
string: "Stairwell::Types::StringType",
|
14
|
+
integer: "Stairwell::Types::IntegerType",
|
15
|
+
boolean: "Stairwell::Types::BooleanType",
|
16
|
+
float: "Stairwell::Types::FloatType",
|
17
|
+
date: "Stairwell::Types::DateType",
|
18
|
+
date_time: "Stairwell::Types::DateTimeType",
|
19
|
+
null: "Stairwell::Types::NullType",
|
20
|
+
column_name: "Stairwell::Types::ColumnNameType",
|
21
|
+
table_name: "Stairwell::Types::TableNameType"
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
# for development and testing
|
25
|
+
unless defined?(Rails)
|
26
|
+
ActiveRecord::Base.establish_connection(
|
27
|
+
adapter: 'sqlite3',
|
28
|
+
database: 'test.db'
|
29
|
+
)
|
30
|
+
end
|
14
31
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Stairwell
|
2
2
|
class BindTransformer
|
3
3
|
|
4
|
-
attr_accessor :sql, :bind_hash, :depleting_bind_hash
|
4
|
+
attr_accessor :sql, :bind_hash, :depleting_bind_hash, :converted_sql
|
5
5
|
|
6
6
|
def initialize(sql, bind_hash)
|
7
7
|
@sql = sql
|
@@ -18,26 +18,29 @@ module Stairwell
|
|
18
18
|
# with quoted values to ensure safety.
|
19
19
|
# Note: $2 is The match for the first, second, etc. parenthesized groups in the last regex
|
20
20
|
def transform
|
21
|
-
|
21
|
+
convert_sql
|
22
|
+
validate_bind_hash
|
23
|
+
converted_sql
|
24
|
+
end
|
25
|
+
|
26
|
+
def convert_sql
|
27
|
+
@converted_sql ||= sql.gsub(/(:?):([a-zA-Z]\w*)/) do |_|
|
22
28
|
replace = $2.to_sym
|
23
29
|
validate_sql(replace)
|
24
|
-
bind_hash[replace].
|
30
|
+
bind_hash[replace].quote
|
25
31
|
end
|
26
|
-
|
27
|
-
validate_bind_hash
|
28
|
-
converted_sql
|
29
32
|
end
|
30
33
|
|
31
34
|
private
|
32
35
|
|
33
36
|
def validate_sql(attr)
|
34
|
-
raise SqlBindMismatch, ":#{attr} in your query is missing from your
|
37
|
+
raise SqlBindMismatch, ":#{attr} in your query is missing from your args" unless bind_hash[attr]
|
35
38
|
|
36
39
|
depleting_bind_hash.delete(attr)
|
37
40
|
end
|
38
41
|
|
39
42
|
def validate_bind_hash
|
40
|
-
raise SqlBindMismatch, "
|
43
|
+
raise SqlBindMismatch, ":#{depleting_bind_hash.keys.join(', ')} in your bind hash is missing from your query: #{sql}" unless depleting_bind_hash.empty?
|
41
44
|
end
|
42
45
|
|
43
46
|
end
|
data/lib/stairwell/query.rb
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
require "stairwell/bind_transformer"
|
2
|
+
require "stairwell/types/boolean_type"
|
3
|
+
require "stairwell/types/column_name_type"
|
4
|
+
require "stairwell/types/date_time_type"
|
5
|
+
require "stairwell/types/date_type"
|
6
|
+
require "stairwell/types/float_type"
|
7
|
+
require "stairwell/types/integer_type"
|
8
|
+
require "stairwell/types/in_type"
|
9
|
+
require "stairwell/types/string_type"
|
10
|
+
require "stairwell/types/null_type"
|
11
|
+
require "stairwell/types/table_name_type"
|
12
|
+
|
1
13
|
module Stairwell
|
2
14
|
class Query
|
3
15
|
|
@@ -26,10 +38,15 @@ module Stairwell
|
|
26
38
|
|
27
39
|
bind_hash.each do |bind_name, bind_value|
|
28
40
|
type = all_validations[bind_name]
|
29
|
-
|
30
|
-
|
41
|
+
if type.is_a?(Array)
|
42
|
+
type = type.first
|
43
|
+
type_object = Types::InType.new(bind_value, type)
|
44
|
+
end
|
45
|
+
type_object ||= Object.const_get(TYPE_CLASSES[type]).new(bind_value)
|
46
|
+
|
47
|
+
raise InvalidBindType.new("#{bind_name} is not #{all_validations[bind_name]}") unless type_object.valid?
|
31
48
|
|
32
|
-
|
49
|
+
bind_hash[bind_name] = type_object
|
33
50
|
end
|
34
51
|
end
|
35
52
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Stairwell::Types
|
2
|
+
class BaseType
|
3
|
+
attr_reader :value
|
4
|
+
|
5
|
+
def initialize(value)
|
6
|
+
@value = value
|
7
|
+
end
|
8
|
+
|
9
|
+
def quote
|
10
|
+
connection.quote(value)
|
11
|
+
end
|
12
|
+
|
13
|
+
def connection
|
14
|
+
@connection ||= ActiveRecord::Base.connection
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "stairwell/types/base_type"
|
2
|
+
|
3
|
+
module Stairwell::Types
|
4
|
+
class InType
|
5
|
+
attr_reader :value, :type
|
6
|
+
|
7
|
+
def initialize(value, type)
|
8
|
+
@value = value
|
9
|
+
@type = type
|
10
|
+
end
|
11
|
+
|
12
|
+
def quote
|
13
|
+
contained_values.map(&:quote).join(", ")
|
14
|
+
end
|
15
|
+
|
16
|
+
def valid?
|
17
|
+
value.is_a?(Array) && contained_values.all?(&:valid?)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def contained_values
|
23
|
+
value.map do |contained|
|
24
|
+
Object.const_get(Stairwell::TYPE_CLASSES[type]).new(contained)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/stairwell/version.rb
CHANGED
data/stairwell.gemspec
CHANGED
@@ -25,4 +25,11 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.bindir = "exe"
|
26
26
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
27
|
spec.require_paths = ["lib"]
|
28
|
+
|
29
|
+
spec.add_dependency 'activerecord', '>= 4.2.11'
|
30
|
+
|
31
|
+
# for development and testing
|
32
|
+
unless defined?(Rails)
|
33
|
+
spec.add_dependency 'sqlite3'
|
34
|
+
end
|
28
35
|
end
|
data/test.db
ADDED
File without changes
|
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stairwell
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- tobyond
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
12
|
-
dependencies:
|
11
|
+
date: 2020-10-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.2.11
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.2.11
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sqlite3
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
13
41
|
description: Sanitize and quote raw SQL for rails and any project in ruby
|
14
42
|
email:
|
15
43
|
executables: []
|
@@ -27,12 +55,21 @@ files:
|
|
27
55
|
- bin/setup
|
28
56
|
- lib/stairwell.rb
|
29
57
|
- lib/stairwell/bind_transformer.rb
|
30
|
-
- lib/stairwell/core_extensions/core.rb
|
31
|
-
- lib/stairwell/core_extensions/types.rb
|
32
58
|
- lib/stairwell/query.rb
|
33
|
-
- lib/stairwell/
|
59
|
+
- lib/stairwell/types/base_type.rb
|
60
|
+
- lib/stairwell/types/boolean_type.rb
|
61
|
+
- lib/stairwell/types/column_name_type.rb
|
62
|
+
- lib/stairwell/types/date_time_type.rb
|
63
|
+
- lib/stairwell/types/date_type.rb
|
64
|
+
- lib/stairwell/types/float_type.rb
|
65
|
+
- lib/stairwell/types/in_type.rb
|
66
|
+
- lib/stairwell/types/integer_type.rb
|
67
|
+
- lib/stairwell/types/null_type.rb
|
68
|
+
- lib/stairwell/types/string_type.rb
|
69
|
+
- lib/stairwell/types/table_name_type.rb
|
34
70
|
- lib/stairwell/version.rb
|
35
71
|
- stairwell.gemspec
|
72
|
+
- test.db
|
36
73
|
homepage: https://github.com/tobyond/stairwell
|
37
74
|
licenses:
|
38
75
|
- MIT
|
@@ -1,77 +0,0 @@
|
|
1
|
-
class String
|
2
|
-
def squish!
|
3
|
-
gsub!(/[[:space:]]+/, " ")
|
4
|
-
strip!
|
5
|
-
self
|
6
|
-
end
|
7
|
-
|
8
|
-
def underscore
|
9
|
-
self.gsub(/::/, '/').
|
10
|
-
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
11
|
-
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
12
|
-
tr("-", "_").
|
13
|
-
downcase
|
14
|
-
end
|
15
|
-
|
16
|
-
def sql_quote
|
17
|
-
"'#{self.gsub('\\', '\&\&').gsub("'", "''")}'"
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
class TrueClass
|
22
|
-
def sql_quote
|
23
|
-
"TRUE"
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
class FalseClass
|
28
|
-
def sql_quote
|
29
|
-
"FALSE"
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
class NilClass
|
34
|
-
def sql_quote
|
35
|
-
"IS NULL"
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
class Integer
|
40
|
-
def sql_quote
|
41
|
-
self
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
class Float
|
46
|
-
def sql_quote
|
47
|
-
self
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
class Array
|
52
|
-
def sql_quote
|
53
|
-
map(&:sql_quote).join(", ")
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
class Date
|
58
|
-
def self.parsable?(string)
|
59
|
-
begin
|
60
|
-
parse(string)
|
61
|
-
true
|
62
|
-
rescue ArgumentError
|
63
|
-
false
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
class DateTime
|
69
|
-
def self.parsable?(string)
|
70
|
-
begin
|
71
|
-
parse(string)
|
72
|
-
true
|
73
|
-
rescue ArgumentError
|
74
|
-
false
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
@@ -1,25 +0,0 @@
|
|
1
|
-
require "stairwell/core_extensions/core"
|
2
|
-
require "stairwell/core_extensions/types"
|
3
|
-
|
4
|
-
module Stairwell
|
5
|
-
class TypeValidator
|
6
|
-
|
7
|
-
class << self
|
8
|
-
TYPES = [
|
9
|
-
String,
|
10
|
-
Boolean,
|
11
|
-
Integer,
|
12
|
-
Float,
|
13
|
-
SqlDate,
|
14
|
-
SqlDateTime
|
15
|
-
].freeze
|
16
|
-
|
17
|
-
TYPES.each do |type|
|
18
|
-
define_method(type.to_s.underscore.to_sym) do |arg|
|
19
|
-
return arg.is_a?(type) unless arg.is_a?(Array)
|
20
|
-
arg.all? { |element| element.is_a?(type) }
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|