squint 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|