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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0e395c41810e9e42ae30e3f2aca5527fe80666fd52dcfdd9913b3501b6e97f0
4
- data.tar.gz: dab379e41ed0fe39a5f90e3dc42b99337d5dbf441e6e8b1b10eae93ab6f202c9
3
+ metadata.gz: 3c5deadfa80337621d1082bbd878ba261d2892c5021371d7f3b1d8a1ddf2f006
4
+ data.tar.gz: fdd8e1ed4bf501c13cfea20e1b02860c6e1c37e525a6052dfd766bb16bffc4fa
5
5
  SHA512:
6
- metadata.gz: 51fc2b15b2016a7030c4fbd63c0cf0065453c1cff0d4a50007d8870e556cf8e4098008f7077a76b85c41124d865cda4e4fda3d9f0acdc38d8dbb4b7c951f32ba
7
- data.tar.gz: e97e619209f362b9412ad7df74b0511f3f99b557dd1085cd8b7508ae4ee369b74e2e8f5a7ea2c4c8191ab3253ecd4432ee8c25b0c2f5d14d295c344b206198bf
6
+ metadata.gz: 60141c5d729d71ba3b4e44501ef8299438bef3c290fd034c6f5714f3d1b3a398ecc0be7c9ace074d24528019d86bddec2275c3103f4db7594a312866c8e5f7b9
7
+ data.tar.gz: 46e6d7c0d0e67a77ee514e7da5a229109cd62b6d32fdd00a7472fb86f7fc9e67b88181fe79e194cf2e756613f5343f2916de23866aa25b29b8b525ffaac65921
data/CHANGELOG.md CHANGED
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2024-12-03
11
+
12
+ - Add basic support for TRIGGERS
13
+
10
14
  ## [0.1.0] - 2024-11-28
11
15
 
12
16
  ### Added
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: "sql", returns: "void", replace: true)
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, if_exists: false, cascade: false)
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
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module FullTextSearch
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  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.1.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-29 00:00:00.000000000 Z
11
+ date: 2024-12-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg