pgmq-ruby 0.6.2 → 0.7.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.
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGMQ
4
+ # Queue name validation, normalization, and sanitization.
5
+ #
6
+ # PGMQ interpolates a queue's name into PostgreSQL identifiers (it creates tables named +pgmq.q_<name>+ and
7
+ # +pgmq.a_<name>+), so a name has to be a valid, length-bounded SQL identifier. This module is the single source of
8
+ # truth for those rules and offers tiers depending on how much you trust the input:
9
+ #
10
+ # 1. {.validate!} / {.valid?} - assert a name is already valid. Use for names you control. {PGMQ::Client} calls
11
+ # {.validate!} before every operation.
12
+ # 2. {.normalize} - lightly rewrite a name that is *meant* to be valid but uses a friendlier separator. Maps the
13
+ # common stream separators (hyphens, dots, colons) to underscores, strips any other invalid character, then
14
+ # validates - so a Turbo Stream channel like +"chat:room-7"+ becomes +"chat_room_7"+. Raises if the result still
15
+ # can't be a valid name (empty, or starts with a digit).
16
+ # 3. {.sanitize!} - coerce *untrusted* input into a valid name by stripping every invalid character, then validate.
17
+ # Raises if nothing valid remains. Use this as a SQL-identifier guard: the result is always either a name you
18
+ # know is safe, or an exception - never a silent substitute.
19
+ # 4. {.sanitize} - the lenient sibling of {.sanitize!}: best-effort coercion that *never* raises for content and
20
+ # always returns a usable identifier (falling back to a default). Convenient, but see the collision caveat on
21
+ # the method - distinct inputs can map to the same name.
22
+ #
23
+ # @example
24
+ # PGMQ::QueueName.valid?("orders") # => true
25
+ # PGMQ::QueueName.validate!("my-queue") # => raises InvalidQueueNameError
26
+ # PGMQ::QueueName.normalize("chat:room-7") # => "chat_room_7"
27
+ # PGMQ::QueueName.sanitize!("orders!!") # => "orders"
28
+ # PGMQ::QueueName.sanitize!("!!!") # => raises InvalidQueueNameError
29
+ # PGMQ::QueueName.sanitize("99 Problems!") # => "q_99_problems"
30
+ module QueueName
31
+ # Maximum queue name length. PGMQ creates tables with prefixes (+q_+, +a_+) and PostgreSQL caps identifiers at 63
32
+ # characters; PGMQ enforces 48 to leave room for those prefixes and suffixes.
33
+ MAX_LENGTH = 48
34
+
35
+ # A valid queue name: starts with a letter or underscore, then letters, digits, or underscores.
36
+ PATTERN = /\A[a-zA-Z_][a-zA-Z0-9_]*\z/
37
+
38
+ # Prefix used by {.sanitize} when the input would otherwise start with an illegal leading character (e.g. a
39
+ # digit) but still has usable trailing characters.
40
+ SANITIZE_PREFIX = "q_"
41
+
42
+ # Name {.sanitize} falls back to when the input has no usable characters at all.
43
+ SANITIZE_FALLBACK = "queue"
44
+
45
+ module_function
46
+
47
+ # Returns true if the name is already a valid queue name.
48
+ #
49
+ # @param name [String, #to_s] candidate queue name
50
+ # @return [Boolean]
51
+ def valid?(name)
52
+ str = name.to_s
53
+ !str.empty? && str.length < MAX_LENGTH && str.match?(PATTERN)
54
+ end
55
+
56
+ # Validates a queue name, returning it unchanged when valid and raising otherwise.
57
+ #
58
+ # @param name [String, #to_s] candidate queue name
59
+ # @return [String] the validated name (as a String)
60
+ # @raise [PGMQ::Errors::InvalidQueueNameError] if the name is empty, too long, or not a valid identifier
61
+ def validate!(name)
62
+ str = name.to_s
63
+
64
+ if name.nil? || str.strip.empty?
65
+ raise Errors::InvalidQueueNameError, "Queue name cannot be empty"
66
+ end
67
+
68
+ if str.length >= MAX_LENGTH
69
+ raise Errors::InvalidQueueNameError,
70
+ "Queue name '#{str}' exceeds maximum length of #{MAX_LENGTH} characters " \
71
+ "(current length: #{str.length})"
72
+ end
73
+
74
+ return str if str.match?(PATTERN)
75
+
76
+ raise Errors::InvalidQueueNameError,
77
+ "Invalid queue name '#{str}': must start with a letter or underscore " \
78
+ "and contain only letters, digits, and underscores"
79
+ end
80
+
81
+ # Rewrites a name that is meant to be valid but uses friendlier separators, then validates it.
82
+ #
83
+ # Maps the common stream-name separators - hyphens, dots, and colons - to underscores, strips any *other* invalid
84
+ # character, then collapses repeated underscores and trims them from the ends. So +"chat:room-7"+ becomes
85
+ # +"chat_room_7"+ and +"order.events"+ becomes +"order_events"+, while a stray +"a@b"+ becomes +"ab"+ (the +@+ is
86
+ # dropped, not turned into a separator). The result is validated, so names that still can't be valid (empty, or
87
+ # starting with a digit) raise rather than being silently mangled.
88
+ #
89
+ # Colons in particular are the turbo-rails stream-name separator, so they are mapped to a safe character rather
90
+ # than stripped - otherwise +"a:b"+ and +"ab"+ would collide on the same queue.
91
+ #
92
+ # @param name [String, #to_s] a name using friendly separators
93
+ # @return [String] the normalized, validated queue name
94
+ # @raise [PGMQ::Errors::InvalidQueueNameError] if the normalized result is not a valid queue name
95
+ def normalize(name)
96
+ str = name.to_s
97
+ return validate!(str) if str.match?(PATTERN)
98
+
99
+ normalized = str
100
+ .gsub(/[-.:]/, "_") # hyphens / dots / colons -> underscores
101
+ .gsub(/[^a-zA-Z0-9_]/, "") # strip any remaining invalid character
102
+ .squeeze("_") # collapse consecutive underscores
103
+ .gsub(/\A_+|_+\z/, "") # trim leading / trailing underscores
104
+
105
+ validate!(normalized)
106
+ end
107
+
108
+ # Strips every invalid character from untrusted input, then validates the result.
109
+ #
110
+ # This is the SQL-identifier guard: it removes anything outside +[A-Za-z0-9_]+ and passes the remainder through
111
+ # {.validate!}. If nothing valid remains (empty result, leading digit, too long) it raises, so a caller can never
112
+ # accidentally operate on a different queue than intended. Use this for names from untrusted sources (URL params,
113
+ # external systems) where a wrong-but-valid name would be worse than an error.
114
+ #
115
+ # @param name [String, #to_s] untrusted input
116
+ # @return [String] the sanitized, validated queue name
117
+ # @raise [PGMQ::Errors::InvalidQueueNameError] if nothing valid remains after stripping
118
+ def sanitize!(name)
119
+ validate!(name.to_s.gsub(/[^a-zA-Z0-9_]/, ""))
120
+ end
121
+
122
+ # Best-effort coercion of arbitrary input into a valid queue name; the lenient sibling of {.sanitize!}.
123
+ #
124
+ # Never raises for content: it lowercases, replaces every illegal character run with an underscore, trims
125
+ # underscores, prefixes {SANITIZE_PREFIX} when the first surviving character is not a valid identifier start (e.g.
126
+ # a digit), truncates to fit {MAX_LENGTH}, and falls back to {SANITIZE_FALLBACK} when nothing usable remains. The
127
+ # return value always satisfies {.valid?}.
128
+ #
129
+ # @note Because it coerces rather than rejects, distinct inputs can map to the *same* name (e.g. +"a/b"+ and
130
+ # +"a-b"+ both become +"a_b"+; +"!!!"+ and +""+ both become +"queue"+). If your name selects a queue table,
131
+ # that means two logically different inputs could share one queue. When that matters - especially for untrusted
132
+ # input - prefer {.sanitize!}, which raises instead of substituting.
133
+ #
134
+ # @param name [String, #to_s] arbitrary input
135
+ # @return [String] a guaranteed-valid queue name
136
+ def sanitize(name)
137
+ cleaned = name.to_s.downcase
138
+ .gsub(/[^a-z0-9_]+/, "_").squeeze("_")
139
+ .gsub(/\A_+|_+\z/, "")
140
+
141
+ # Nothing usable survived the scrub - use the fallback rather than emit a bare prefix like "q_".
142
+ return SANITIZE_FALLBACK if cleaned.empty?
143
+
144
+ # A valid identifier can't start with a digit; prefix so the leading character is legal.
145
+ cleaned = "#{SANITIZE_PREFIX}#{cleaned}" unless cleaned.match?(/\A[a-z_]/)
146
+
147
+ # Keep under MAX_LENGTH (valid? requires strictly less than), trimming any underscore left at the cut.
148
+ cleaned = cleaned[0, MAX_LENGTH - 1].sub(/_+\z/, "") if cleaned.length >= MAX_LENGTH
149
+
150
+ cleaned.empty? ? SANITIZE_FALLBACK : cleaned
151
+ end
152
+ end
153
+ end
@@ -3,12 +3,11 @@
3
3
  module PGMQ
4
4
  # Low-level transaction support for PGMQ operations
5
5
  #
6
- # Provides atomic execution of PGMQ operations within PostgreSQL transactions.
7
- # Transactions are a database primitive - this is a thin wrapper around
8
- # PostgreSQL's native transaction support.
6
+ # Provides atomic execution of PGMQ operations within PostgreSQL transactions. Transactions are a database primitive -
7
+ # this is a thin wrapper around PostgreSQL's native transaction support.
9
8
  #
10
- # This is analogous to rdkafka-ruby providing Kafka transaction support -
11
- # it's a protocol/database feature, not a framework abstraction.
9
+ # This is analogous to rdkafka-ruby providing Kafka transaction support - it's a protocol/database feature, not a
10
+ # framework abstraction.
12
11
  #
13
12
  # @example Atomic multi-queue operations
14
13
  # client.transaction do |txn|
@@ -24,8 +23,8 @@ module PGMQ
24
23
  module Transaction
25
24
  # Executes PGMQ operations atomically within a database transaction
26
25
  #
27
- # Obtains a connection from the pool, starts a transaction, and yields
28
- # a client that uses that transaction for all operations.
26
+ # Obtains a connection from the pool, starts a transaction, and yields a client that uses that transaction for all
27
+ # operations.
29
28
  #
30
29
  # @yield [PGMQ::Client] transactional client using the same connection
31
30
  # @return [Object] result of the block
data/lib/pgmq/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module PGMQ
4
4
  # Current version of the pgmq-ruby gem
5
- VERSION = "0.6.2"
5
+ VERSION = "0.7.0"
6
6
  end
data/lib/pgmq.rb CHANGED
@@ -12,9 +12,8 @@ loader.eager_load
12
12
 
13
13
  # PGMQ - Low-level Ruby client for Postgres Message Queue
14
14
  #
15
- # This is a low-level library providing direct access to PGMQ operations.
16
- # For higher-level abstractions, job processing, and framework integrations,
17
- # see pgmq-framework (similar to how rdkafka-ruby relates to Karafka).
15
+ # This is a low-level library providing direct access to PGMQ operations. For higher-level abstractions, job processing,
16
+ # and framework integrations, see pgmq-framework (similar to how rdkafka-ruby relates to Karafka).
18
17
  #
19
18
  # @example Basic usage
20
19
  # require 'pgmq'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgmq-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -64,6 +64,7 @@ files:
64
64
  - README.md
65
65
  - lib/pgmq.rb
66
66
  - lib/pgmq/client.rb
67
+ - lib/pgmq/client/autovacuum.rb
67
68
  - lib/pgmq/client/consumer.rb
68
69
  - lib/pgmq/client/maintenance.rb
69
70
  - lib/pgmq/client/message_lifecycle.rb
@@ -76,7 +77,9 @@ files:
76
77
  - lib/pgmq/errors.rb
77
78
  - lib/pgmq/message.rb
78
79
  - lib/pgmq/metrics.rb
80
+ - lib/pgmq/notify_throttle.rb
79
81
  - lib/pgmq/queue_metadata.rb
82
+ - lib/pgmq/queue_name.rb
80
83
  - lib/pgmq/transaction.rb
81
84
  - lib/pgmq/version.rb
82
85
  - pgmq-ruby.gemspec
@@ -104,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
104
107
  - !ruby/object:Gem::Version
105
108
  version: '0'
106
109
  requirements: []
107
- rubygems_version: 4.0.6
110
+ rubygems_version: 4.0.10
108
111
  specification_version: 4
109
112
  summary: Ruby client for PGMQ (Postgres Message Queue)
110
113
  test_files: []