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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +28 -0
- data/lib/squint.rb +209 -0
- data/lib/squint/version.rb +3 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/models/post.rb +14 -0
- data/test/dummy/app/models/post.rb~ +2 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +25 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +14 -0
- data/test/dummy/config/database.yml~ +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/db/migrate/20170512185941_create_posts.rb +19 -0
- data/test/dummy/db/migrate/20170512185941_create_posts.rb~ +12 -0
- data/test/dummy/db/schema.rb +36 -0
- data/test/dummy/log/development.log +351 -0
- data/test/dummy/log/test.log +27199 -0
- data/test/dummy/test/fixtures/posts.yml +65 -0
- data/test/dummy/test/fixtures/posts.yml~ +13 -0
- data/test/dummy/test/models/post_test.rb +4 -0
- data/test/dummy/test/models/post_test.rb~ +17 -0
- data/test/squint_test.rb +103 -0
- data/test/test_helper.rb +29 -0
- data/test/test_helper.rb~ +20 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -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
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/lib/squint.rb
ADDED
@@ -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
|
data/test/dummy/Rakefile
ADDED
@@ -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,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,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,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
|