worker_plugins 0.0.14 → 0.0.15
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/app/services/worker_plugins/add_query.rb +36 -19
- data/app/services/worker_plugins/remove_query.rb +28 -8
- data/app/services/worker_plugins/select_column_with_type_cast.rb +14 -4
- data/app/services/worker_plugins/switch_query.rb +23 -13
- data/lib/worker_plugins/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 46a8d25556ad9d41f62307376bf457a790ab3e67e934678cac0ad9b754112e8e
|
|
4
|
+
data.tar.gz: 2163df96c0f85a9102f1694fbeda59f06f70c296924e0a9dd9320a0b239e0f15
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef92d06893d7b9a1e56ade70c4b78432c0cd3b04544db92c5827865883d97b9912fc392930e53d26324c6bb6dbec23481e101fb4ba0218c51a2e52ef1634c497
|
|
7
|
+
data.tar.gz: 24c4313891e0864b95695af807eca5586739c997039cc3bb54b82050964af2dbd92806c90b3e7077d8acdd93f5b636dd306e6300331961b41ef71d424f8a099a
|
|
@@ -8,17 +8,11 @@ class WorkerPlugins::AddQuery < WorkerPlugins::ApplicationService
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def perform
|
|
11
|
-
|
|
12
|
-
add_query_to_workplace
|
|
13
|
-
succeed!(created:)
|
|
11
|
+
succeed!(affected_count: add_query_to_workplace)
|
|
14
12
|
end
|
|
15
13
|
|
|
16
14
|
def add_query_to_workplace
|
|
17
|
-
WorkerPlugins::WorkplaceLink.connection.
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def created
|
|
21
|
-
@created ||= resources_to_add.pluck(primary_key.to_sym)
|
|
15
|
+
WorkerPlugins::WorkplaceLink.connection.exec_update(sql, "WorkerPlugins::AddQuery INSERT", [])
|
|
22
16
|
end
|
|
23
17
|
|
|
24
18
|
def ids_added_already_query
|
|
@@ -40,19 +34,25 @@ class WorkerPlugins::AddQuery < WorkerPlugins::ApplicationService
|
|
|
40
34
|
end
|
|
41
35
|
|
|
42
36
|
def primary_key
|
|
43
|
-
@primary_key ||=
|
|
37
|
+
@primary_key ||= model_class.primary_key
|
|
44
38
|
end
|
|
45
39
|
|
|
46
40
|
def resources_to_add
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
41
|
+
# The unique index on `(workplace_id, resource_type, resource_id)` lets us
|
|
42
|
+
# skip the `WHERE NOT EXISTS` anti-join for the common unbounded query —
|
|
43
|
+
# duplicates are rejected on INSERT by the dialect-specific conflict
|
|
44
|
+
# clause in #sql. `.distinct` still handles same-row duplicates produced
|
|
45
|
+
# by joins in the caller's query (e.g. `User.joins(:tasks)`).
|
|
46
|
+
#
|
|
47
|
+
# When the caller scopes with `.limit` / `.offset`, we keep the anti-join
|
|
48
|
+
# so already-linked rows are filtered *before* the window is applied;
|
|
49
|
+
# otherwise `Task.limit(100)` could insert fewer than 100 new rows when
|
|
50
|
+
# some of those 100 are already linked.
|
|
51
|
+
@resources_to_add ||= if query.limit_value || query.offset_value
|
|
52
|
+
query.distinct.where("NOT EXISTS (#{existing_workplace_link_exists_sql})")
|
|
53
|
+
else
|
|
54
|
+
query.distinct
|
|
55
|
+
end
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def existing_workplace_link_exists_sql
|
|
@@ -106,7 +106,7 @@ class WorkerPlugins::AddQuery < WorkerPlugins::ApplicationService
|
|
|
106
106
|
|
|
107
107
|
def sql
|
|
108
108
|
@sql ||= "
|
|
109
|
-
|
|
109
|
+
#{insert_clause} INTO
|
|
110
110
|
worker_plugins_workplace_links
|
|
111
111
|
|
|
112
112
|
(
|
|
@@ -118,6 +118,23 @@ class WorkerPlugins::AddQuery < WorkerPlugins::ApplicationService
|
|
|
118
118
|
)
|
|
119
119
|
|
|
120
120
|
#{select_sql}
|
|
121
|
+
#{conflict_clause}
|
|
121
122
|
"
|
|
122
123
|
end
|
|
124
|
+
|
|
125
|
+
def insert_clause
|
|
126
|
+
if mysql?
|
|
127
|
+
"INSERT IGNORE"
|
|
128
|
+
elsif sqlite?
|
|
129
|
+
"INSERT OR IGNORE"
|
|
130
|
+
else
|
|
131
|
+
"INSERT"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def conflict_clause
|
|
136
|
+
return "" unless postgres?
|
|
137
|
+
|
|
138
|
+
"ON CONFLICT (workplace_id, resource_type, resource_id) DO NOTHING"
|
|
139
|
+
end
|
|
123
140
|
end
|
|
@@ -1,17 +1,37 @@
|
|
|
1
1
|
class WorkerPlugins::RemoveQuery < WorkerPlugins::ApplicationService
|
|
2
2
|
arguments :query, :workplace
|
|
3
3
|
|
|
4
|
-
attr_reader :destroyed
|
|
5
|
-
|
|
6
4
|
def perform
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
succeed!(affected_count: links_scope.delete_all)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def links_scope
|
|
9
|
+
scope = workplace.workplace_links.where(resource_type: model_class.name)
|
|
10
|
+
return scope if unscoped_query?
|
|
11
|
+
|
|
12
|
+
scope.where(resource_id: query_with_selected_ids)
|
|
9
13
|
end
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
# If the caller's query has no meaningful scoping applied, the `resource_id
|
|
16
|
+
# IN (SELECT ... FROM <target_table>)` subquery would simply materialize
|
|
17
|
+
# every row of the target model — for 340k+ users that's a full-table scan
|
|
18
|
+
# with no semantic effect other than preserving orphaned links. The
|
|
19
|
+
# `resource_type = ?` filter alone is enough to pin the DELETE to this
|
|
20
|
+
# workplace's links of the given type, so we short-circuit the subquery in
|
|
21
|
+
# that case. Orphaned links (whose resource row has since been deleted) are
|
|
22
|
+
# deleted alongside live ones, which matches caller intent ("remove
|
|
23
|
+
# everything matching the query") and is the correct thing to do with
|
|
24
|
+
# dead references anyway.
|
|
25
|
+
def unscoped_query?
|
|
26
|
+
@query.where_clause.empty? &&
|
|
27
|
+
@query.joins_values.empty? &&
|
|
28
|
+
@query.left_outer_joins_values.empty? &&
|
|
29
|
+
@query.group_values.empty? &&
|
|
30
|
+
@query.having_clause.empty? &&
|
|
31
|
+
@query.limit_value.nil? &&
|
|
32
|
+
@query.offset_value.nil? &&
|
|
33
|
+
@query.from_clause.value.nil? &&
|
|
34
|
+
@query.with_values.empty?
|
|
15
35
|
end
|
|
16
36
|
|
|
17
37
|
def model_class
|
|
@@ -46,14 +46,24 @@ class WorkerPlugins::SelectColumnWithTypeCast < WorkerPlugins::ApplicationServic
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def same_type?
|
|
49
|
-
column_to_select.type == column_to_compare_with.type
|
|
49
|
+
return true if column_to_select.type == column_to_compare_with.type
|
|
50
|
+
|
|
51
|
+
mysql_implicit_conversion_safe?
|
|
50
52
|
end
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
# On MySQL / MariaDB, implicit conversion handles comparisons between any
|
|
55
|
+
# string-ish column types (VARCHAR, CHAR, BINARY, UUID, or types AR doesn't
|
|
56
|
+
# recognize — e.g. MariaDB's native UUID type when using an older mysql2
|
|
57
|
+
# adapter). The explicit CAST is only needed when one side is numeric,
|
|
58
|
+
# because MySQL would then force a string → number conversion that loses
|
|
59
|
+
# rows containing non-numeric values.
|
|
60
|
+
def mysql_implicit_conversion_safe?
|
|
53
61
|
return false unless mysql?
|
|
54
62
|
|
|
55
|
-
|
|
63
|
+
!numeric_type?(column_to_select.type) && !numeric_type?(column_to_compare_with.type)
|
|
64
|
+
end
|
|
56
65
|
|
|
57
|
-
|
|
66
|
+
def numeric_type?(type)
|
|
67
|
+
%i[integer decimal float bigint].include?(type)
|
|
58
68
|
end
|
|
59
69
|
end
|
|
@@ -2,20 +2,30 @@ class WorkerPlugins::SwitchQuery < WorkerPlugins::ApplicationService
|
|
|
2
2
|
arguments :query, :workplace
|
|
3
3
|
|
|
4
4
|
def perform
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
# Decide mode *before* running the insert. Deciding it from AddQuery's
|
|
6
|
+
# post-insert `affected_count` would make concurrent "add" toggles
|
|
7
|
+
# destructive: if request A's INSERT commits first, overlapping
|
|
8
|
+
# request B would see `affected_count == 0` from its own (no-op)
|
|
9
|
+
# INSERT and flip to RemoveQuery, wiping out what A just added. A
|
|
10
|
+
# pre-insert EXISTS probe keeps the race window small in the same
|
|
11
|
+
# way the previous candidate-pluck approach did, without materializing
|
|
12
|
+
# any ids into Ruby.
|
|
13
|
+
if any_unlinked_candidate?
|
|
14
|
+
add_result = WorkerPlugins::AddQuery.execute!(query:, workplace:)
|
|
15
|
+
succeed!(affected_count: add_result.fetch(:affected_count), mode: :created)
|
|
14
16
|
else
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
mode: :created
|
|
18
|
-
)
|
|
17
|
+
remove_result = WorkerPlugins::RemoveQuery.execute!(query:, workplace:)
|
|
18
|
+
succeed!(affected_count: remove_result.fetch(:affected_count), mode: :destroyed)
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
def any_unlinked_candidate?
|
|
23
|
+
add_service = WorkerPlugins::AddQuery.new(query:, workplace:)
|
|
24
|
+
|
|
25
|
+
add_service
|
|
26
|
+
.query
|
|
27
|
+
.distinct
|
|
28
|
+
.where("NOT EXISTS (#{add_service.existing_workplace_link_exists_sql})")
|
|
29
|
+
.exists?
|
|
30
|
+
end
|
|
21
31
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: worker_plugins
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kasper Stöckel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Rails framework for easily choosing and creating lists of objects and
|
|
14
14
|
execute plugins against them.
|