stairwell 0.1.2 → 0.2.0
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 +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
|