activerecord-full_text_search 0.1.0 → 0.2.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 +4 -0
- data/README.md +6 -0
- data/lib/active_record/full_text_search/7.2/postgresql_adapter.rb +41 -0
- data/lib/active_record/full_text_search/7.2/schema_dumper.rb +25 -0
- data/lib/active_record/full_text_search/command_recorder.rb +16 -0
- data/lib/active_record/full_text_search/schema_statements.rb +68 -2
- data/lib/active_record/full_text_search/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: 3c5deadfa80337621d1082bbd878ba261d2892c5021371d7f3b1d8a1ddf2f006
|
4
|
+
data.tar.gz: fdd8e1ed4bf501c13cfea20e1b02860c6e1c37e525a6052dfd766bb16bffc4fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 60141c5d729d71ba3b4e44501ef8299438bef3c290fd034c6f5714f3d1b3a398ecc0be7c9ace074d24528019d86bddec2275c3103f4db7594a312866c8e5f7b9
|
7
|
+
data.tar.gz: 46e6d7c0d0e67a77ee514e7da5a229109cd62b6d32fdd00a7472fb86f7fc9e67b88181fe79e194cf2e756613f5343f2916de23866aa25b29b8b525ffaac65921
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -10,6 +10,8 @@ The gem permits to use these commands:
|
|
10
10
|
|
11
11
|
- `create_function`
|
12
12
|
- `drop_function`
|
13
|
+
- `create_trigger`
|
14
|
+
- `drop_trigger`
|
13
15
|
- `create_text_search_template`
|
14
16
|
- `rename_text_search_template`
|
15
17
|
- `drop_text_search_template`
|
@@ -31,6 +33,10 @@ The gem permits to use these commands:
|
|
31
33
|
|
32
34
|
- Add tests
|
33
35
|
- Enhance (and extract?) functions support
|
36
|
+
- Enhance (and extract?) triggers support
|
37
|
+
- Support triggers with parameterized functions
|
38
|
+
- Support `UPDATE OF columns` in triggers
|
39
|
+
- Support `TRUNCATE` in triggers
|
34
40
|
- Check recorder
|
35
41
|
- Manage schema (`public` is hardcoded)
|
36
42
|
|
@@ -31,6 +31,47 @@ module ActiveRecord
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
+
# See https://github.com/postgres/postgres/blob/master/src/include/commands/trigger.h
|
35
|
+
# and https://stackoverflow.com/questions/23634550/meanings-of-bits-in-trigger-type-field-tgtype-of-postgres-pg-trigger
|
36
|
+
def triggers
|
37
|
+
# List of triggers in the current schema with name, table name, function, timing, op, for each, condition, deferrable, initially_deferred.
|
38
|
+
res = exec_query(<<-SQL.strip_heredoc, "SCHEMA")
|
39
|
+
SELECT tgname, c.relname, proname,
|
40
|
+
COALESCE(
|
41
|
+
CASE WHEN (tgtype::int::bit(7) & b'0000010')::int = 0 THEN NULL ELSE 'before' END,
|
42
|
+
CASE WHEN (tgtype::int::bit(7) & b'0000010')::int = 0 THEN 'after' ELSE NULL END,
|
43
|
+
CASE WHEN (tgtype::int::bit(7) & b'1000000')::int = 0 THEN NULL ELSE 'instead_of' END,
|
44
|
+
''
|
45
|
+
) as tg_timing,
|
46
|
+
(CASE WHEN (tgtype::int::bit(7) & b'0000100')::int = 0 THEN '' ELSE ' insert' END)
|
47
|
+
|| (CASE WHEN (tgtype::int::bit(7) & b'0001000')::int = 0 THEN '' ELSE ' delete' END)
|
48
|
+
|| (CASE WHEN (tgtype::int::bit(7) & b'0010000')::int = 0 THEN '' ELSE ' update' END)
|
49
|
+
-- || (CASE WHEN (tgtype::int::bit(7) & b'0100000')::int = 0 THEN '' ELSE ' truncate' END)
|
50
|
+
AS tg_ops,
|
51
|
+
CASE WHEN (tgtype::int::bit(7) & b'0000001')::int = 0 THEN 'statement' ELSE 'row' END as tg_foreach,
|
52
|
+
pg_get_expr(tgqual, tgrelid) AS tg_condition,
|
53
|
+
tgdeferrable,
|
54
|
+
tginitdeferred
|
55
|
+
FROM pg_catalog.pg_trigger t
|
56
|
+
JOIN pg_catalog.pg_class c ON c.oid = tgrelid
|
57
|
+
JOIN pg_catalog.pg_proc f ON f.oid = t.tgfoid
|
58
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
59
|
+
LEFT JOIN pg_catalog.pg_depend d ON d.objid = t.oid AND d.deptype = 'e'
|
60
|
+
LEFT JOIN pg_catalog.pg_extension e ON e.oid = d.refobjid
|
61
|
+
WHERE n.nspname = ANY (current_schemas(false))
|
62
|
+
AND tgisinternal = FALSE
|
63
|
+
AND e.extname IS NULL;
|
64
|
+
SQL
|
65
|
+
|
66
|
+
res.rows.each_with_object({}) do |(name, table, function, timing, ops, for_each, condition, deferrable, initially_deferred), memo|
|
67
|
+
attributes = {table: table, function: function, for_each: for_each.to_sym}
|
68
|
+
attributes[:when] = condition if condition.present?
|
69
|
+
attributes[timing.to_sym] = ops.strip.split(/\s+/).map(&:to_sym)
|
70
|
+
attributes[:deferrable] = initially_deferred ? :initially_deferred : true if deferrable
|
71
|
+
memo[name] = attributes
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
34
75
|
def text_search_parsers
|
35
76
|
res = exec_query(<<-SQL.strip_heredoc, "SCHEMA")
|
36
77
|
SELECT prsname, prsstart::VARCHAR, prstoken::VARCHAR, prsend::VARCHAR, prsheadline::VARCHAR, prslextype::VARCHAR
|
@@ -17,6 +17,11 @@ module ActiveRecord
|
|
17
17
|
text_search_configurations(stream)
|
18
18
|
end
|
19
19
|
|
20
|
+
def tables(stream)
|
21
|
+
super
|
22
|
+
triggers(stream)
|
23
|
+
end
|
24
|
+
|
20
25
|
def functions(stream)
|
21
26
|
return unless (functions = @connection.functions).any?
|
22
27
|
|
@@ -33,6 +38,22 @@ module ActiveRecord
|
|
33
38
|
stream.puts
|
34
39
|
end
|
35
40
|
|
41
|
+
def triggers(stream)
|
42
|
+
return unless (triggers = @connection.triggers).any?
|
43
|
+
|
44
|
+
stream.puts
|
45
|
+
stream.puts " # These are triggers that must be created in order to support this database"
|
46
|
+
|
47
|
+
triggers.each do |name, definition|
|
48
|
+
definition[:name] = name if include_trigger_name_in_dump?(definition)
|
49
|
+
table = definition.delete(:table)
|
50
|
+
function = definition.delete(:function)
|
51
|
+
stream.puts %( create_trigger #{table.inspect}, #{function.inspect}, #{hash_to_string(definition)})
|
52
|
+
end
|
53
|
+
|
54
|
+
# stream.puts
|
55
|
+
end
|
56
|
+
|
36
57
|
def text_search_parsers(stream)
|
37
58
|
return unless (parsers = @connection.text_search_parsers).any?
|
38
59
|
|
@@ -88,6 +109,10 @@ module ActiveRecord
|
|
88
109
|
|
89
110
|
private
|
90
111
|
|
112
|
+
def include_trigger_name_in_dump?(trigger_definition)
|
113
|
+
trigger_definition[:name] !=~ /\Atg_rails_/
|
114
|
+
end
|
115
|
+
|
91
116
|
def hash_to_string(hash)
|
92
117
|
hash.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
|
93
118
|
end
|
@@ -30,6 +30,14 @@ module ActiveRecord
|
|
30
30
|
record(:drop_function, args, &block)
|
31
31
|
end
|
32
32
|
|
33
|
+
def create_trigger(*args, &block)
|
34
|
+
record(:create_trigger, args, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def drop_trigger(*args, &block)
|
38
|
+
record(:drop_trigger, args, &block)
|
39
|
+
end
|
40
|
+
|
33
41
|
def create_text_search_configuration(*args, &block)
|
34
42
|
record(:create_text_search_configuration, args, &block)
|
35
43
|
end
|
@@ -108,6 +116,14 @@ module ActiveRecord
|
|
108
116
|
[:create_function, args]
|
109
117
|
end
|
110
118
|
|
119
|
+
def invert_create_trigger(args)
|
120
|
+
[:drop_trigger, args]
|
121
|
+
end
|
122
|
+
|
123
|
+
def invert_drop_trigger(args)
|
124
|
+
[:create_trigger, args]
|
125
|
+
end
|
126
|
+
|
111
127
|
def invert_create_text_search_configuration(args)
|
112
128
|
[:drop_text_search_configuration, args]
|
113
129
|
end
|
@@ -6,7 +6,10 @@ module ActiveRecord
|
|
6
6
|
end
|
7
7
|
|
8
8
|
module SchemaStatements
|
9
|
-
def create_function(name_with_args, as:, volatility: :volatile, language:
|
9
|
+
def create_function(name_with_args, as:, volatility: :volatile, language: nil, returns: :void, replace: true)
|
10
|
+
name_with_args = "#{name_with_args}()" unless name_with_args.to_s.include?("(")
|
11
|
+
language = "plpgsql" if returns == :trigger
|
12
|
+
language ||= "sql"
|
10
13
|
execute(<<-SQL)
|
11
14
|
CREATE #{"OR REPLACE" if replace} FUNCTION #{name_with_args}
|
12
15
|
RETURNS #{returns}
|
@@ -18,10 +21,60 @@ module ActiveRecord
|
|
18
21
|
SQL
|
19
22
|
end
|
20
23
|
|
21
|
-
def drop_function(name_with_args,
|
24
|
+
def drop_function(name_with_args, options = {})
|
25
|
+
if_exists = options[:if_exists]
|
26
|
+
cascade = options[:cascade]
|
22
27
|
execute "DROP FUNCTION #{"IF EXISTS" if if_exists} #{name_with_args} #{"CASCADE" if cascade}"
|
23
28
|
end
|
24
29
|
|
30
|
+
def create_trigger(table, function, options = {})
|
31
|
+
raise ArgumentError, "function name is invalid" unless /\A\w+\z/.match?(function.to_s)
|
32
|
+
raise ArgumentError, "Must specify one and only one of the options :before, :after, or :instead_of" unless %i[before after instead_of].select { |t| options.key?(t) }.count == 1
|
33
|
+
raise ArgumentError, "for_each must be :row or :statement" if options[:for_each] && !%i[row statement].include?(options[:for_each])
|
34
|
+
|
35
|
+
timing = %i[before after instead_of].find { |t| options.key?(t) }
|
36
|
+
operations = options[timing] || raise(ArgumentError, "Must specify operations for #{timing} trigger")
|
37
|
+
operations.detect { |op| %i[insert update delete].exclude?(op) } && raise(ArgumentError, "Invalid operation for trigger: #{operations.inspect}")
|
38
|
+
for_each = "FOR EACH #{options[:for_each].to_s.upcase}" if options[:for_each]
|
39
|
+
if options[:deferrable] == :initially_deferred
|
40
|
+
deferrability = "DEFERRABLE INITIALLY DEFERRED"
|
41
|
+
elsif options[:deferrable] == :initially_immediate
|
42
|
+
deferrability = "DEFERRABLE INITIALLY IMMEDIATE"
|
43
|
+
elsif options[:deferrable] == true
|
44
|
+
deferrability = "DEFERRABLE"
|
45
|
+
elsif options[:deferrable] == false
|
46
|
+
deferrability = "NOT DEFERRABLE"
|
47
|
+
elsif options[:deferrable]
|
48
|
+
raise ArgumentError, "Invalid value for :deferrable"
|
49
|
+
end
|
50
|
+
condition = options[:when] ? "WHEN (#{options[:when]})" : ""
|
51
|
+
operations = [operations].flatten.map do |event|
|
52
|
+
if %i[insert update delete].include?(event)
|
53
|
+
event.to_s.upcase
|
54
|
+
# elsif event.is_a?(Hash)
|
55
|
+
# raise ArgumentError, "Key must be :update" unless event.keys.size == 1 && event.keys.first == :update
|
56
|
+
# "UPDATE OF #{[event[:update]].flatten.map { |c| quote_column_name(c) }.join(", ")}"
|
57
|
+
else
|
58
|
+
raise ArgumentError, "Unsupported event: #{event.inspect}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
name = options[:name] || default_trigger_name(table, function, timing, operations)
|
62
|
+
|
63
|
+
execute "CREATE TRIGGER #{name} #{timing.to_s.upcase} #{operations.join(" OR ")} ON #{table} #{for_each} #{deferrability} #{condition} EXECUTE FUNCTION #{function}()"
|
64
|
+
end
|
65
|
+
|
66
|
+
def drop_trigger(table, function, options = {})
|
67
|
+
if_exists = options[:if_exists]
|
68
|
+
cascade = options[:cascade]
|
69
|
+
if options.keys.intersect?(%i[before after instead_of])
|
70
|
+
raise ArgumentError, "Must specify only one of the options :before, :after, or :instead_of" unless %i[before after instead_of].select { |t| options.key?(t) }.count == 1
|
71
|
+
timing = %i[before after instead_of].find { |t| options.key?(t) }
|
72
|
+
operations = options[timing] || raise(ArgumentError, "Must specify operations for #{timing} trigger")
|
73
|
+
end
|
74
|
+
name = options[:name] || default_trigger_name(table, function, timing, operations)
|
75
|
+
execute "DROP TRIGGER #{"IF EXISTS" if if_exists} #{name} ON #{table} #{"CASCADE" if cascade}"
|
76
|
+
end
|
77
|
+
|
25
78
|
def create_text_search_template(name, lexize:, init: nil)
|
26
79
|
options = {init: init, lexize: lexize}.compact
|
27
80
|
execute("CREATE TEXT SEARCH TEMPLATE public.#{name} (#{options.map { |k, v| "#{k.upcase} = #{v}" }.join(", ")})")
|
@@ -96,6 +149,19 @@ module ActiveRecord
|
|
96
149
|
def drop_text_search_configuration(name, if_exists: false, cascade: :restrict)
|
97
150
|
execute "DROP TEXT SEARCH CONFIGURATION #{"IF EXISTS" if if_exists} public.#{name} #{"CASCADE" if cascade == :cascade}"
|
98
151
|
end
|
152
|
+
|
153
|
+
def max_trigger_name_size
|
154
|
+
62
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
# Copied from ActiveRecord::ConnectionAdapters::Abstract::SchemaStatements#foreign_key_name
|
160
|
+
def default_trigger_name(table, function, timing, operations)
|
161
|
+
identifier = "#{table}_#{function}_#{timing}_#{operations.sort.join('_')}_tg".underscore
|
162
|
+
hashed_identifier = OpenSSL::Digest::SHA256.hexdigest(identifier).first(10)
|
163
|
+
"tg_rails_#{hashed_identifier}"
|
164
|
+
end
|
99
165
|
end
|
100
166
|
end
|
101
167
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-full_text_search
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Codeur SAS
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-12-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pg
|