activerecord-sqlite-types 0.3.3 → 0.4.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/CHANGELOG.md +6 -0
- data/README.md +29 -1
- data/lib/activerecord-sqlite-types.rb +3 -0
- data/lib/sqlite_types/array.rb +4 -3
- data/lib/sqlite_types/array_columns.rb +137 -0
- data/lib/sqlite_types/version.rb +1 -1
- data/sig/sqlite_types.rbs +3 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b2ce466647cec30a5ddb1f87e7f2721fad43f8a8de5eeeba8bf19172ef8386a
|
|
4
|
+
data.tar.gz: fb6289b9981c9e0e79ef4db08aa8436906310b4c0a8849dba8406176a31064f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 417f05eb69a1df1a10effc7af883c3beb2130248fd94c23f7bc628323d195e09bb8072e9ad27d7eb34e7a737ce0c9e5b809af649c5c356e58ef753388a871186
|
|
7
|
+
data.tar.gz: 02b49246f0ecb7ee52c17de2392710f6e99a6ed3e3f4cc7966c396ea44b29bb9487795f027291d5d19b89d7aba7d4aff90f76fc4da2d3b5e3ea361500634bca8
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -115,7 +115,35 @@ end
|
|
|
115
115
|
|
|
116
116
|
**Querying arrays:**
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
Include `SQLiteTypes::ArrayColumns` in models where you want tag-style querying helpers for JSON-backed array columns:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
class Event < ApplicationRecord
|
|
122
|
+
include SQLiteTypes::ArrayColumns
|
|
123
|
+
|
|
124
|
+
attribute :relationship_statuses, SQLiteTypes::Array.new(:string)
|
|
125
|
+
array_columns :relationship_statuses
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Declaring an array column adds sanitization before validation, class aggregate methods, scopes, and instance predicates:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
Event.unique_relationship_statuses
|
|
133
|
+
Event.relationship_statuses_cloud
|
|
134
|
+
Event.with_relationship_statuses
|
|
135
|
+
Event.without_relationship_statuses
|
|
136
|
+
Event.with_any_relationship_statuses("single", "partnered")
|
|
137
|
+
Event.with_all_relationship_statuses("single", "partnered")
|
|
138
|
+
Event.without_any_relationship_statuses("single")
|
|
139
|
+
Event.without_all_relationship_statuses("single", "partnered")
|
|
140
|
+
|
|
141
|
+
event.has_any_relationship_statuses?("single", "partnered")
|
|
142
|
+
event.has_all_relationship_statuses?("single", "partnered")
|
|
143
|
+
event.has_relationship_status?("single")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Values are compacted, stringified, deduplicated, and sorted before validation and query comparison. The scopes use SQLite's `JSON_EACH` function and are based on [Stephen Margheim's article on enhancing Rails SQLite array columns](https://fractaledmind.com/2023/09/12/enhancing-rails-sqlite-array-columns/).
|
|
119
147
|
|
|
120
148
|
### Interval
|
|
121
149
|
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
require "active_record"
|
|
2
|
+
require "active_support/concern"
|
|
3
|
+
require "active_support/core_ext/object/blank"
|
|
2
4
|
require "active_support/time"
|
|
3
5
|
require "date"
|
|
4
6
|
require "ipaddr"
|
|
5
7
|
require_relative "sqlite_types/version"
|
|
6
8
|
require_relative "sqlite_types/ip_address"
|
|
7
9
|
require_relative "sqlite_types/array"
|
|
10
|
+
require_relative "sqlite_types/array_columns"
|
|
8
11
|
require_relative "sqlite_types/interval"
|
|
9
12
|
require_relative "sqlite_types/migration_helpers"
|
|
10
13
|
|
data/lib/sqlite_types/array.rb
CHANGED
|
@@ -92,10 +92,11 @@ module SQLiteTypes
|
|
|
92
92
|
elem
|
|
93
93
|
when :hash
|
|
94
94
|
return elem if json_compatible?(elem)
|
|
95
|
-
return json_round_trip(elem) if elem.is_a?(::Hash)
|
|
96
95
|
|
|
97
96
|
serialized = json_round_trip(elem)
|
|
98
|
-
|
|
97
|
+
unless serialized.is_a?(::Hash) && serialized.any?
|
|
98
|
+
raise ArgumentError, "Invalid #{@subtype} array element: #{elem.inspect}"
|
|
99
|
+
end
|
|
99
100
|
|
|
100
101
|
serialized
|
|
101
102
|
when :json, :jsonb
|
|
@@ -204,7 +205,7 @@ module SQLiteTypes
|
|
|
204
205
|
end
|
|
205
206
|
|
|
206
207
|
def json_round_trip(value)
|
|
207
|
-
|
|
208
|
+
ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(value))
|
|
208
209
|
rescue JSON::ParserError, TypeError, EncodingError
|
|
209
210
|
raise ArgumentError, "Invalid #{@subtype} array element: #{value.inspect}"
|
|
210
211
|
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SQLiteTypes
|
|
4
|
+
module ArrayColumns
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
class_methods do
|
|
8
|
+
def array_columns_sanitize_list(values = [])
|
|
9
|
+
return [] if values.nil?
|
|
10
|
+
|
|
11
|
+
values.select(&:present?).map(&:to_s).uniq.sort
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def array_columns(*column_names)
|
|
15
|
+
@array_columns ||= {}
|
|
16
|
+
|
|
17
|
+
array_columns_sanitize_list(column_names).each do |column_name|
|
|
18
|
+
@array_columns[column_name] ||= false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@array_columns.each do |column_name, initialized|
|
|
22
|
+
next if initialized
|
|
23
|
+
|
|
24
|
+
define_array_column_methods(column_name)
|
|
25
|
+
@array_columns[column_name] = true
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def define_array_column_methods(column_name)
|
|
32
|
+
method_name = column_name.downcase
|
|
33
|
+
json_each = Arel::Nodes::NamedFunction.new("JSON_EACH", [arel_table[column_name]])
|
|
34
|
+
|
|
35
|
+
define_array_column_aggregate_methods(method_name, json_each)
|
|
36
|
+
define_array_column_presence_scopes(method_name, column_name)
|
|
37
|
+
define_array_column_query_scopes(method_name, json_each)
|
|
38
|
+
define_array_column_predicates(method_name, column_name)
|
|
39
|
+
define_array_column_writer(column_name)
|
|
40
|
+
|
|
41
|
+
before_validation -> { self[column_name] = self.class.array_columns_sanitize_list(self[column_name]) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def define_array_column_aggregate_methods(method_name, json_each)
|
|
45
|
+
define_singleton_method :"unique_#{method_name}" do |_conditions = "true"|
|
|
46
|
+
select("value")
|
|
47
|
+
.from([arel_table, json_each])
|
|
48
|
+
.distinct
|
|
49
|
+
.pluck("value")
|
|
50
|
+
.sort
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
define_singleton_method :"#{method_name}_cloud" do |_conditions = "true"|
|
|
54
|
+
select("value")
|
|
55
|
+
.from([arel_table, json_each])
|
|
56
|
+
.group("value")
|
|
57
|
+
.order("value")
|
|
58
|
+
.pluck(Arel.sql("value, COUNT(*) AS count"))
|
|
59
|
+
.to_h
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def define_array_column_presence_scopes(method_name, column_name)
|
|
64
|
+
scope :"with_#{method_name}", -> {
|
|
65
|
+
where.not(arel_table[column_name].eq(nil))
|
|
66
|
+
.where.not(arel_table[column_name].eq([]))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
scope :"without_#{method_name}", -> {
|
|
70
|
+
where(arel_table[column_name].eq(nil))
|
|
71
|
+
.or(where(arel_table[column_name].eq([])))
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def define_array_column_query_scopes(method_name, json_each)
|
|
76
|
+
overlap_query = ->(items) {
|
|
77
|
+
values = array_columns_sanitize_list(items)
|
|
78
|
+
|
|
79
|
+
Arel::SelectManager.new(json_each)
|
|
80
|
+
.project(1)
|
|
81
|
+
.where(Arel.sql("CAST(value AS TEXT)").in(values))
|
|
82
|
+
.take(1)
|
|
83
|
+
.exists
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
contains_query = ->(items) {
|
|
87
|
+
values = array_columns_sanitize_list(items)
|
|
88
|
+
count = Arel::SelectManager.new(json_each)
|
|
89
|
+
.project(Arel.sql("value").count(true))
|
|
90
|
+
.where(Arel.sql("CAST(value AS TEXT)").in(values))
|
|
91
|
+
|
|
92
|
+
Arel::Nodes::Equality.new(count, values.size)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
scope :"with_any_#{method_name}", ->(*items) {
|
|
96
|
+
where overlap_query.call(items)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
scope :"with_all_#{method_name}", ->(*items) {
|
|
100
|
+
where contains_query.call(items)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
scope :"without_any_#{method_name}", ->(*items) {
|
|
104
|
+
where.not overlap_query.call(items)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
scope :"without_all_#{method_name}", ->(*items) {
|
|
108
|
+
where.not contains_query.call(items)
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def define_array_column_writer(column_name)
|
|
113
|
+
define_method :"#{column_name}=" do |value|
|
|
114
|
+
super(self.class.array_columns_sanitize_list(value))
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def define_array_column_predicates(method_name, column_name)
|
|
119
|
+
define_method :"has_any_#{method_name}?" do |*values|
|
|
120
|
+
values = self.class.array_columns_sanitize_list(values)
|
|
121
|
+
existing = self.class.array_columns_sanitize_list(self[column_name])
|
|
122
|
+
|
|
123
|
+
(values & existing).present?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
define_method :"has_all_#{method_name}?" do |*values|
|
|
127
|
+
values = self.class.array_columns_sanitize_list(values)
|
|
128
|
+
existing = self.class.array_columns_sanitize_list(self[column_name])
|
|
129
|
+
|
|
130
|
+
(values & existing).size == values.size
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
alias_method :"has_#{method_name.singularize}?", :"has_all_#{method_name}?"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
data/lib/sqlite_types/version.rb
CHANGED
data/sig/sqlite_types.rbs
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-sqlite-types
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Wojtek Wrona
|
|
@@ -83,6 +83,7 @@ files:
|
|
|
83
83
|
- lib/generators/sqlite_types/migration/templates/migration.rb.tt
|
|
84
84
|
- lib/sqlite_types.rb
|
|
85
85
|
- lib/sqlite_types/array.rb
|
|
86
|
+
- lib/sqlite_types/array_columns.rb
|
|
86
87
|
- lib/sqlite_types/interval.rb
|
|
87
88
|
- lib/sqlite_types/ip_address.rb
|
|
88
89
|
- lib/sqlite_types/migration_helpers.rb
|