stairwell 0.1.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7846f146e7d718b2c1dacaaf795e2093c3f24a5a4ee520a2821c6a3bb23d2ca2
4
- data.tar.gz: 3b39deebbb1db67dd063953d92899b4cc4b94a96fda740a0c6b2173ef28e7c24
3
+ metadata.gz: e963ad4c2388946d0baae50090e6f3e9913e49dcdd896cdd18403861d72b9a40
4
+ data.tar.gz: 29cb2a589732e6e4b0b43115da878d8b4463eb59a1ad46726d01595bc0f56f19
5
5
  SHA512:
6
- metadata.gz: 1e678fe8b202b64ba0d2abbd1771983fc2a72c9a757478c6cfac163af7985b68f8e2d47458a01715b81c756153f6858963cb6d3d59ed9223d089ff6fdebeb01e
7
- data.tar.gz: 7fe23548a55c4ea1eb3f10d1ab8eb375699f7849e084af9346eee041fef19c5129558952b662d4814adbb0439f5e5af17bce02f27884f58fc0e8c3adf01dc483
6
+ metadata.gz: 0d21bd2ad1fc8a8b04de7da744dae787a474a33fbb42146a4b77d218d5ee493c25ed930da39b23a4ce45a67448920924e5d2a4006ba62978422caf3c43156395
7
+ data.tar.gz: 229bc47578fea9e530d10291d79d94bcd042222e767f5627b45e4b7867af09e76958a34b9d4208b6763c1d89fcea5399317ad6c4047c547e2bae11e62361f61c
data/Gemfile.lock CHANGED
@@ -1,18 +1,52 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- stairwell (0.1.1)
4
+ stairwell (0.3.0)
5
+ activerecord (>= 4.2.11)
6
+ sqlite3
5
7
 
6
8
  GEM
7
9
  remote: https://rubygems.org/
8
10
  specs:
11
+ activemodel (7.1.1)
12
+ activesupport (= 7.1.1)
13
+ activerecord (7.1.1)
14
+ activemodel (= 7.1.1)
15
+ activesupport (= 7.1.1)
16
+ timeout (>= 0.4.0)
17
+ activesupport (7.1.1)
18
+ base64
19
+ bigdecimal
20
+ concurrent-ruby (~> 1.0, >= 1.0.2)
21
+ connection_pool (>= 2.2.5)
22
+ drb
23
+ i18n (>= 1.6, < 2)
24
+ minitest (>= 5.1)
25
+ mutex_m
26
+ tzinfo (~> 2.0)
27
+ base64 (0.1.1)
28
+ bigdecimal (3.1.4)
9
29
  coderay (1.1.3)
30
+ concurrent-ruby (1.2.2)
31
+ connection_pool (2.4.1)
32
+ drb (2.1.1)
33
+ ruby2_keywords
34
+ i18n (1.14.1)
35
+ concurrent-ruby (~> 1.0)
10
36
  method_source (1.0.0)
11
- minitest (5.14.1)
37
+ mini_portile2 (2.8.5)
38
+ minitest (5.20.0)
39
+ mutex_m (0.1.2)
12
40
  pry (0.13.1)
13
41
  coderay (~> 1.1)
14
42
  method_source (~> 1.0)
15
43
  rake (12.3.3)
44
+ ruby2_keywords (0.0.5)
45
+ sqlite3 (1.6.7)
46
+ mini_portile2 (~> 2.8.0)
47
+ timeout (0.4.0)
48
+ tzinfo (2.0.6)
49
+ concurrent-ruby (~> 1.0)
16
50
 
17
51
  PLATFORMS
18
52
  ruby
data/README.md CHANGED
@@ -18,9 +18,15 @@ Or install it yourself as:
18
18
 
19
19
  $ gem install stairwell
20
20
 
21
+ ## Why?
22
+ Although ActiveRecord serves as an excellent tool for the majority of database queries, certain scenarios call for more customized queries.
23
+ This project was initially conceived to help transition a development team and their project from PHP to Ruby. This PHP project had thousands of complex SQL queries, thus the necessity of making SQL a first-class citizen in the ruby project enabled a smoother transition.
24
+ So, why not Arel? Arel is a powerful tool, but it's worth noting that it is considered a private API and is likely to remain so for the forseeable future.
25
+ Does this approach make queries less composable? Yes, if you are used to chaining your arel queries and AR scopes then you're probably not going to use this. However, it provides an interface that enables you to leverage SQL securely in your Ruby projects without the need to reinvent the wheel.
26
+
21
27
  ## Usage
22
28
 
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 not a dependency here.
29
+ 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
30
  In rails you could create a directory called `app/queries` for instance.
25
31
 
26
32
  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 +40,7 @@ class UsersSql < Stairwell::Query
34
40
  validate_type :gpa, :float
35
41
  validate_type :date_joined, :sql_date
36
42
  validate_type :created_at, :sql_date_time
43
+ validate_type :favorite_numbers, [:integer]
37
44
 
38
45
  query <<-SQL
39
46
  SELECT
@@ -45,6 +52,7 @@ class UsersSql < Stairwell::Query
45
52
  AND gpa = :gpa
46
53
  AND date_joined = :date_joined
47
54
  AND created_at >= :created_at
55
+ AND favorite_numbers IN (:favorite_numbers)
48
56
  ;
49
57
  SQL
50
58
  end
@@ -55,9 +63,10 @@ binds = {
55
63
  name: "First",
56
64
  age: 99,
57
65
  active: true,
58
- gpa: 4.2
66
+ gpa: 4.2,
59
67
  date_joined: "2008-08-28",
60
68
  created_at: "2008-08-28 23:41:18",
69
+ favorite_numbers: [4, 7, 100]
61
70
  }
62
71
 
63
72
  # and call the following:
@@ -66,23 +75,33 @@ UsersSql.sql(binds)
66
75
 
67
76
  # You will receive the following result:
68
77
 
69
- "SELECT * FROM users WHERE name = 'First' age = 99 active = TRUE date_joined = '2008-08-28' created_at = '2008-08-28 23:41:18' gpa = 4.2;"
78
+ "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
79
  ```
71
80
 
72
81
  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
82
  They types of the binds are validated too.
74
83
  The names binds in your sql are also validated.
75
- Strings are quoted. Dates are quoted. Datetime is quoted.
76
-
77
- Tested in both Mysql and Postgres.
84
+ All types are quoted using ActiveRecord quoting, which will be different depending on your database type (Mysql, postgres etc.)
85
+
86
+ ## Supported Types
87
+
88
+ | Type | Values Accepted | Info |
89
+ |--------------|----------------------|------------------------------------------------------------------------------------------------------|
90
+ | :boolean | TrueClass/FalseClass | Not fully supported since many databases require 'IS TRUE' or 'IS FALSE' |
91
+ | :column_name | String | for quoting a column name |
92
+ | :date_time | String | only taking the actual string for now |
93
+ | :date | String | only taking the actual string for now |
94
+ | :float | Float | |
95
+ | [<type>] | Array | will quote any type provided in the array [:integer] |
96
+ | :integer | Integer | |
97
+ | :null | NilClass | nil/NULL values are not completely supported since many databases require 'IS NULL' or 'IS NOT NULL' |
98
+ | :string | String | |
99
+ | :table_name | String | for quoting a table name |
78
100
 
79
101
  ## Known issues
80
102
 
81
- * nil/NULL values are not currently supported. Just use `IS NULL` or `IS NOT NULL` in your query for the time being.
103
+ * 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
104
  * 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
105
 
87
106
 
88
107
  ## Development
@@ -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
- converted_sql = sql.gsub(/(:?):([a-zA-Z]\w*)/) do |_|
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].sql_quote
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 bind hash: #{bind_hash}" unless bind_hash[attr]
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, "#{depleting_bind_hash} in your bind hash is missing from your query: #{sql}" unless depleting_bind_hash.empty?
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
@@ -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
- type = type.first if type.is_a?(Array)
30
- valid = TypeValidator.send(type, bind_value)
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
- raise InvalidBindType.new("#{bind_name} is not #{all_validations[bind_name]}") unless valid
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,9 @@
1
+ require "stairwell/types/base_type"
2
+
3
+ module Stairwell::Types
4
+ class BooleanType < BaseType
5
+ def valid?
6
+ value.is_a?(TrueClass) || value.is_a?(FalseClass)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ require "stairwell/types/base_type"
2
+
3
+ module Stairwell::Types
4
+ class ColumnNameType < BaseType
5
+ def valid?
6
+ value.is_a?(String)
7
+ end
8
+
9
+ def quote
10
+ connection.quote_column_name(value)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ require "stairwell/types/base_type"
2
+
3
+ module Stairwell::Types
4
+ class DateTimeType < BaseType
5
+ def valid?
6
+ value.is_a?(String)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require "stairwell/types/base_type"
2
+
3
+ module Stairwell::Types
4
+ class DateType < BaseType
5
+ def valid?
6
+ value.is_a?(String)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require "stairwell/types/base_type"
2
+
3
+ module Stairwell::Types
4
+ class FloatType < BaseType
5
+ def valid?
6
+ value.is_a?(Float)
7
+ end
8
+ end
9
+ 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
@@ -0,0 +1,9 @@
1
+ require "stairwell/types/base_type"
2
+
3
+ module Stairwell::Types
4
+ class IntegerType < BaseType
5
+ def valid?
6
+ value.is_a?(Integer)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require "stairwell/types/base_type"
2
+
3
+ module Stairwell::Types
4
+ class NullType < BaseType
5
+ def valid?
6
+ value.is_a?(NilClass)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require "stairwell/types/base_type"
2
+
3
+ module Stairwell::Types
4
+ class StringType < BaseType
5
+ def valid?
6
+ value.is_a?(String)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ require "stairwell/types/base_type"
2
+
3
+ module Stairwell::Types
4
+ class TableNameType < BaseType
5
+ def valid?
6
+ value.is_a?(String)
7
+ end
8
+
9
+ def quote
10
+ connection.quote_table_name(value)
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,3 @@
1
1
  module Stairwell
2
- VERSION = "0.1.2"
2
+ VERSION = "0.3.0"
3
3
  end
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
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.1.2
4
+ version: 0.3.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-09-21 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2023-10-25 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/type_validator.rb
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
@@ -54,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
54
91
  - !ruby/object:Gem::Version
55
92
  version: '0'
56
93
  requirements: []
57
- rubygems_version: 3.1.2
94
+ rubygems_version: 3.4.10
58
95
  signing_key:
59
96
  specification_version: 4
60
97
  summary: stairwell for sql
@@ -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,7 +0,0 @@
1
- module Boolean; end
2
- class TrueClass; include Boolean; end
3
- class FalseClass; include Boolean; end
4
-
5
- module SqlDate; end
6
- module SqlDateTime; end
7
- class String; include SqlDate; include SqlDateTime; 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