squint 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fec97dc85dd083e4a957f2f4096b0ea02292582b
4
+ data.tar.gz: 53764ef0ad978f8cc7f9e8c85703166abda7d262
5
+ SHA512:
6
+ metadata.gz: 332e6d79f31fc3e799e012cf6cf828156bc0c292aff29a306677c719a97699f64a113531c8a844ed96efefaa2294916ada5dfc0812748b51dad72fe388e4d7b1
7
+ data.tar.gz: 81c2aedc39702bbe42bdbb9f106331c13420dbb157a2f84f44189df35d6fbe7022ca651a29bbcdd4083ac508a431048f71d74a29acb832ea20b750281e8a2070
@@ -0,0 +1,20 @@
1
+ Copyright 2017 David H. Wilkins
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.
@@ -0,0 +1,28 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Squint'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ Bundler::GemHelper.install_tasks
18
+
19
+ require 'rake/testtask'
20
+
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << 'lib'
23
+ t.libs << 'test'
24
+ t.pattern = 'test/**/*_test.rb'
25
+ t.verbose = false
26
+ end
27
+
28
+ task default: :test
@@ -0,0 +1,209 @@
1
+ require 'active_support/concern'
2
+
3
+ # Squint json, jsonb, hstore queries
4
+ module Squint
5
+ extend ActiveSupport::Concern
6
+ include ::ActiveRecord::QueryMethods
7
+
8
+ module WhereMethods
9
+ # Args may be passed to build_where like:
10
+ # build_where(jsonb_column: {key1: value1})
11
+ # build_where(jsonb_column: {key1: value1}, jsonb_column: {key2: value2})
12
+ # build_where(jsonb_column: {key1: value1}, regular_column: value)
13
+ # build_where(jsonb_column: {key1: value1}, association: {column: value))
14
+ def build_where(*args)
15
+ args.inject([]) do |memo, arg|
16
+ if arg.is_a?(Hash)
17
+ arg.keys.each do |key|
18
+ if arg[key].is_a?(Hash) && HASH_DATA_COLUMNS[key]
19
+ memo << hash_field_reln(key => arg[key])
20
+ else
21
+ memo += super(key => arg[key])
22
+ end
23
+ end
24
+ elsif arg.present?
25
+ memo += super(arg)
26
+ end
27
+ memo
28
+ end
29
+ end
30
+
31
+ # hash_field_reln
32
+ # return an Arel object with the appropriate query
33
+ # Strings want to be a SQL Literal, other things can be
34
+ # passed in bare to the eq or in operator
35
+ def hash_field_reln(*args)
36
+ temp_attr = args[0]
37
+ contains_nil = false
38
+ column_type = HASH_DATA_COLUMNS[args[0].keys.first]
39
+ column_name_segments = []
40
+ quote_char = '"'.freeze
41
+ while temp_attr.is_a?(Hash)
42
+ attribute_sym = temp_attr.keys.first.to_sym
43
+ column_name_segments << (quote_char + temp_attr.keys.first.to_s + quote_char)
44
+ quote_char = '\''.freeze
45
+ temp_attr = temp_attr[temp_attr.keys.first]
46
+ end
47
+
48
+ check_attr_missing = squint_storext_default?(temp_attr, attribute_sym)
49
+
50
+ # Check for nil in array
51
+ if temp_attr.is_a? Array
52
+ contains_nil = temp_attr.include?(nil)
53
+ # remove the nil from the array - we'll handle that later
54
+ temp_attr.compact!
55
+ # if the Array is now just 1 element, it doesn't need to be
56
+ # an Array any longer
57
+ temp_attr = temp_attr[0] if temp_attr.size == 1
58
+ end
59
+
60
+ if temp_attr.is_a? Array
61
+ temp_attr = temp_attr.map(&:to_s)
62
+ elsif ![FalseClass, TrueClass, NilClass].include?(temp_attr.class)
63
+ temp_attr = temp_attr.to_s
64
+ end
65
+
66
+ query_value = if [Array, NilClass].include?(temp_attr.class)
67
+ temp_attr
68
+ else # strings or string-like things
69
+ Arel::Nodes::Quoted.new(temp_attr.to_s)
70
+ end
71
+ # column_name_segments[0] = column_name_segments[0]
72
+ attribute_selector = column_name_segments.join('->'.freeze)
73
+
74
+ # JSON(B) data needs to have the last accessor be ->> instead of
75
+ # -> . The ->> returns the data as text instead of jsonb.
76
+ # hstore columns generally don't have nested keys / hashes
77
+ # Possibly need to raise an error if the hash for an hstore
78
+ # column references nested arrays?
79
+ unless column_type == 'hstore'.freeze
80
+ attribute_selector[attribute_selector.rindex('>'.freeze)] = '>>'.freeze
81
+ end
82
+
83
+ reln = if query_value.is_a?(Array)
84
+ arel_table[Arel::Nodes::SqlLiteral.new(attribute_selector)].in(query_value)
85
+ else
86
+ arel_table[Arel::Nodes::SqlLiteral.new(attribute_selector)].eq(query_value)
87
+ end
88
+
89
+ # If a nil is present in an Array, need add a specific IS NULL comparison
90
+ if contains_nil
91
+ reln = Arel::Nodes::Grouping.new(
92
+ reln.or(arel_table[Arel::Nodes::SqlLiteral.new(attribute_selector)].eq(nil))
93
+ )
94
+ end
95
+
96
+ # check_attr_missing for StoreXT attributes where the default is
97
+ # specified as a query value
98
+ if check_attr_missing
99
+ reln = if column_type == 'hstore'.freeze
100
+ hstore_element_missing(column_name_segments, reln)
101
+ else
102
+ jsonb_element_missing(column_name_segments, reln)
103
+ end
104
+ end
105
+ reln
106
+ end
107
+ end
108
+
109
+ included do |base|
110
+ ar_reln_module = base::ActiveRecord_Relation
111
+ ar_association_module = base::ActiveRecord_AssociationRelation
112
+
113
+ # put together a list of columns in this model
114
+ # that are hstore, json, or jsonb and will benefit from
115
+ # searchability
116
+ HASH_DATA_COLUMNS = base.columns_hash.keys.map do |col_name|
117
+ if %w[hstore json jsonb].include?(base.columns_hash[col_name].sql_type)
118
+ [col_name.to_sym, base.columns_hash[col_name].sql_type]
119
+ end
120
+ end.compact.to_h
121
+
122
+ ar_reln_module.class_eval do
123
+ prepend WhereMethods
124
+ end
125
+
126
+ ar_association_module.class_eval do
127
+ prepend WhereMethods
128
+ end
129
+
130
+ def self.jsonb_element_missing(column_name_segments, reln)
131
+ element = column_name_segments.pop
132
+ attribute_hash_column = column_name_segments.join('->'.freeze)
133
+ # Query generated is equals default or attribute present is null or equals false
134
+ # * Is null happens when the the whole column is null
135
+ # * equals false is when the column has jsonb data, but the key doesn't exist
136
+ # ("posts"."storext_attributes"->>'is_awesome' = 'false' OR
137
+ # (("posts"."storext_attributes" ? 'is_awesome') IS NULL OR
138
+ # ("posts"."storext_attributes" ? 'is_awesome') = FALSE)
139
+ # )
140
+ Arel::Nodes::Grouping.new(
141
+ reln.or(
142
+ Arel::Nodes::Grouping.new(
143
+ Arel::Nodes::Equality.new(
144
+ Arel::Nodes::Grouping.new(
145
+ Arel::Nodes::InfixOperation.new(
146
+ Arel::Nodes::SqlLiteral.new('?'),
147
+ arel_table[Arel::Nodes::SqlLiteral.new(attribute_hash_column)],
148
+ Arel::Nodes::SqlLiteral.new(element)
149
+ )
150
+ ), nil
151
+ ).or(
152
+ Arel::Nodes::Equality.new(
153
+ Arel::Nodes::Grouping.new(
154
+ Arel::Nodes::InfixOperation.new(
155
+ Arel::Nodes::SqlLiteral.new('?'),
156
+ arel_table[Arel::Nodes::SqlLiteral.new(attribute_hash_column)],
157
+ Arel::Nodes::SqlLiteral.new(element)
158
+ )
159
+ ), Arel::Nodes::False.new
160
+ )
161
+ )
162
+ )
163
+ )
164
+ )
165
+ end
166
+
167
+ def self.squint_storext_default?(temp_attr, attribute_sym)
168
+ return false unless respond_to?(:storext_definitions)
169
+ if storext_definitions.keys.include?(attribute_sym) &&
170
+ !storext_definitions[attribute_sym].dig(:opts, :default).nil? &&
171
+ [temp_attr].compact.map(&:to_s).
172
+ flatten.
173
+ include?(storext_definitions[attribute_sym][:opts][:default].to_s)
174
+ true
175
+ end
176
+ end
177
+
178
+ def self.hstore_element_missing(column_name_segments, reln)
179
+ element = column_name_segments.pop
180
+ attribute_hash_column = column_name_segments.join('->'.freeze)
181
+ # Query generated is equals default or attribute present is null or equals false
182
+ # * Is null happens the the column is null
183
+ # * equals false is when the column has jsonb data, but the key doesn't exist
184
+ # ("posts"."storext_attributes"->>'is_awesome' = 'false' OR
185
+ # (exists("posts"."storext_attributes", 'is_awesome') IS NULL OR
186
+ # exists("posts"."storext_attributes", 'is_awesome') = FALSE)
187
+ # )
188
+ Arel::Nodes::Grouping.new(
189
+ reln.or(
190
+ Arel::Nodes::Grouping.new(
191
+ Arel::Nodes::NamedFunction.new(
192
+ "exist",
193
+ [arel_table[Arel::Nodes::SqlLiteral.new(attribute_hash_column)],
194
+ Arel::Nodes::SqlLiteral.new(element)]
195
+ ).eq(Arel::Nodes::False.new)
196
+ ).or(
197
+ Arel::Nodes::Equality.new(
198
+ Arel::Nodes::NamedFunction.new(
199
+ "exist",
200
+ [arel_table[Arel::Nodes::SqlLiteral.new(attribute_hash_column)],
201
+ Arel::Nodes::SqlLiteral.new(element)]
202
+ ), nil
203
+ )
204
+ )
205
+ )
206
+ )
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,3 @@
1
+ module Squint
2
+ VERSION = "0.0.1".freeze
3
+ end
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,14 @@
1
+ class Post < ActiveRecord::Base
2
+ include Storext.model
3
+ include Squint
4
+
5
+ store_attribute :storext_jsonb_attributes, :jsonb_zip_code, String, default: '90210'
6
+ store_attribute :storext_jsonb_attributes, :jsonb_friend_count, Integer, default: 0
7
+ store_attribute :storext_jsonb_attributes, :jsonb_is_awesome, Integer, default: false
8
+ store_attribute :storext_jsonb_attributes, :jsonb_is_present, Integer, default: nil
9
+
10
+ store_attribute :storext_hstore_attributes, :hstore_zip_code, String, default: '90210'
11
+ store_attribute :storext_hstore_attributes, :hstore_friend_count, Integer, default: 0
12
+ store_attribute :storext_hstore_attributes, :hstore_is_awesome, Integer, default: false
13
+ store_attribute :storext_hstore_attributes, :hstore_is_present, Integer, default: nil
14
+ end
@@ -0,0 +1,2 @@
1
+ class Post < ActiveRecord::Base
2
+ end
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Rails.application
@@ -0,0 +1,25 @@
1
+ require File.expand_path('../boot', __FILE__)
2
+
3
+ require 'rails/all'
4
+
5
+ Bundler.require(*Rails.groups)
6
+ require "squint"
7
+
8
+ module Dummy
9
+ class Application < Rails::Application
10
+ # Settings in config/environments/* take precedence over those specified here.
11
+ # Application configuration should go into files in config/initializers
12
+ # -- all .rb files in that directory are automatically loaded.
13
+
14
+ # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
15
+ # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
16
+ # config.time_zone = 'Central Time (US & Canada)'
17
+
18
+ # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
19
+ # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
20
+ # config.i18n.default_locale = :de
21
+
22
+ # Do not swallow errors in after_commit/after_rollback callbacks.
23
+ config.active_record.raise_in_transactional_callbacks = true
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ # Set up gems listed in the Gemfile.
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
3
+
4
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
5
+ $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
@@ -0,0 +1,14 @@
1
+ # local
2
+ default: &default
3
+ adapter: postgresql
4
+ encoding: unicode
5
+ pool: 5
6
+
7
+ development:
8
+ <<: *default
9
+ database: squint_development
10
+
11
+ test:
12
+ <<: *default
13
+ database: squint_test
14
+
@@ -0,0 +1,25 @@
1
+ # SQLite version 3.x
2
+ # gem install sqlite3
3
+ #
4
+ # Ensure the SQLite 3 gem is defined in your Gemfile
5
+ # gem 'sqlite3'
6
+ #
7
+ default: &default
8
+ adapter: sqlite3
9
+ pool: 5
10
+ timeout: 5000
11
+
12
+ development:
13
+ <<: *default
14
+ database: db/development.sqlite3
15
+
16
+ # Warning: The database defined as "test" will be erased and
17
+ # re-generated from your development database when you run "rake".
18
+ # Do not set this db to the same as development or production.
19
+ test:
20
+ <<: *default
21
+ database: db/test.sqlite3
22
+
23
+ production:
24
+ <<: *default
25
+ database: db/production.sqlite3
@@ -0,0 +1,5 @@
1
+ # Load the Rails application.
2
+ require File.expand_path('../application', __FILE__)
3
+
4
+ # Initialize the Rails application.
5
+ Rails.application.initialize!
@@ -0,0 +1,42 @@
1
+ Rails.application.configure do
2
+ # Settings specified here will take precedence over those in config/application.rb.
3
+
4
+ # The test environment is used exclusively to run your application's
5
+ # test suite. You never need to work with it otherwise. Remember that
6
+ # your test database is "scratch space" for the test suite and is wiped
7
+ # and recreated between test runs. Don't rely on the data there!
8
+ config.cache_classes = true
9
+
10
+ # Do not eager load code on boot. This avoids loading your whole application
11
+ # just for the purpose of running a single test. If you are using a tool that
12
+ # preloads Rails for running tests, you may have to set it to true.
13
+ config.eager_load = false
14
+
15
+ # Configure static file server for tests with Cache-Control for performance.
16
+ config.serve_static_files = true
17
+ config.static_cache_control = 'public, max-age=3600'
18
+
19
+ # Show full error reports and disable caching.
20
+ config.consider_all_requests_local = true
21
+ config.action_controller.perform_caching = false
22
+
23
+ # Raise exceptions instead of rendering exception templates.
24
+ config.action_dispatch.show_exceptions = false
25
+
26
+ # Disable request forgery protection in test environment.
27
+ config.action_controller.allow_forgery_protection = false
28
+
29
+ # Tell Action Mailer not to deliver emails to the real world.
30
+ # The :test delivery method accumulates sent emails in the
31
+ # ActionMailer::Base.deliveries array.
32
+ config.action_mailer.delivery_method = :test
33
+
34
+ # Randomize the order test cases are executed.
35
+ config.active_support.test_order = :random
36
+
37
+ # Print deprecation notices to the stderr.
38
+ config.active_support.deprecation = :stderr
39
+
40
+ # Raises error for missing translations
41
+ # config.action_view.raise_on_missing_translations = true
42
+ end
@@ -0,0 +1,19 @@
1
+ class CreatePosts < ActiveRecord::Migration
2
+ def change
3
+ enable_extension "hstore"
4
+ create_table :posts do |t|
5
+ t.string :title
6
+ t.string :body
7
+ t.jsonb :request_info
8
+ t.hstore :properties
9
+ t.jsonb :storext_jsonb_attributes
10
+ t.hstore :storext_hstore_attributes
11
+
12
+ t.timestamps null: false
13
+ t.index :request_info, using: 'GIN'
14
+ t.index :properties, using: 'GIN'
15
+ t.index :storext_jsonb_attributes, using: 'GIN'
16
+ t.index :storext_hstore_attributes, using: 'GIN'
17
+ end
18
+ end
19
+ end