active_record_pretty_key 0.1.1 → 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: fa0c741c251d8d3acde833ff8620ffbea369f74544d1feda3507684596d327de
4
- data.tar.gz: 9be0117695b9d2268c7ba3e930a28983ea50fb19326ce5aabfc6e914bda3c8c5
3
+ metadata.gz: defcd22218e95c247f2e0a177c016feb952f473cc236e5c0d3ec637323b909c6
4
+ data.tar.gz: ffaf93a43f824d0612806c5b3924174ee0e4bfc4aa4f35aa9d4a04d506413956
5
5
  SHA512:
6
- metadata.gz: 4bd0c1ac8f56f770bd90e010467f3aae5e345f316554d657ef7086b7ff288825a5b9a2545c261c495bede196445b54f0566b5d8dd8d8e53ba17926f3bce167aa
7
- data.tar.gz: 6c3e23c905e21f50d625dd22b84d26232f99222d473d79afc4f89e05db2ccc2f4830ccc8a6f1910fe4b5da5e00da733fc98c66664ce741c5a65341a2b38fa2b0
6
+ metadata.gz: a81a35ffd83be05b4fd2918731a3fbdec9d2dd532500af0c8a6fe75eb433c13f7b771c26091f58ef2a1c8986d5db5e9d78edd4e95b27d807e6f6a77bef385b95
7
+ data.tar.gz: dcdbdfcd838af938bff255dbb7a3304fac2a8e6929a6356128e7874ddf3e408d851c55c4d6e343d694d5c9128f00362aa3cd348630bbd612398268ce08aa4a86
data/CHANGELOG.md ADDED
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] - 2026-11-07
9
+
10
+ ### Fixed
11
+ - Custom Sqids alphabet from Rails credentials is now read lazily on first use; previously it was read when the gem loaded (before `Rails.application` exists) and silently ignored
12
+ - PostgreSQL ticket generation uses `nextval()` and no longer holds a row lock on the tickets table for the duration of the caller's transaction
13
+ - MySQL via the Trilogy adapter is now supported; previously it fell through to a fallback that could hand out duplicate IDs under concurrency
14
+ - SQLite ticket generation uses the connection-scoped `last_insert_rowid()` instead of reading the last row
15
+ - `generate_pretty_key` is now public, as documented, and returns the record's key
16
+ - MySQL ticket generation no longer deadlocks under concurrency: concurrent `REPLACE INTO` statements deadlock on InnoDB, so it now uses `UPDATE ... SET id = LAST_INSERT_ID(id + 1)`
17
+ - MySQL tickets are allocated on a dedicated connection pool and commit independently of the caller's transaction, so creates no longer wait for each other's transactions to commit
18
+ - Ticket generation no longer leaves the thread's database connection checked out on Rails 7.2+
19
+ - Requiring the gem no longer loads `ActiveRecord::Base` early (before the app's ActiveRecord configuration is applied)
20
+ - `ActiveRecordPrettyKey::VERSION` is now defined when the gem is required
21
+ - Install generator uses Rails' migration numbering and refuses to create a duplicate migration; the generated migration can be rolled back
22
+
23
+ ### Changed
24
+ - Unsupported database adapters now raise `ActiveRecordPrettyKey::Error` instead of using an unsafe fallback
25
+ - README: corrected security claims and documented that the alphabet must not change once records exist
26
+ - Requires Ruby 3.0+ (as the `sqids` dependency always has), ActiveRecord 6.1 – 8.x and, for MySQL, 8.0+
27
+ - On MySQL, each process may open up to `pool` additional connections for ticket generation
28
+ - Gem releases require RubyGems MFA; README, LICENSE and CHANGELOG are now packaged
29
+ - Development: specs run against SQLite, PostgreSQL, MySQL (mysql2 and Trilogy) on Ruby 3.3, 3.4 and 4.0 in CI; added `bin/console`; fixed `bin/setup` and `bin/build`
30
+
31
+ ### Removed
32
+ - Broken `Thorfile` and `bin/active_record_pretty_key` executable (neither could load the generator), and the `thor` dependency
33
+
34
+ ## [0.1.1] - 2025-08-25
35
+
36
+ ### Fixed
37
+ - Fixed Gemspec homepage
38
+
39
+ ## [0.1.0] - 2025-08-25
40
+
41
+ ### Added
42
+ - Initial gem structure
43
+ - Basic concern `ActiveRecordPrettyKey::Concern` module for ActiveRecord models
44
+ - `generate_pretty_key` instance method
45
+ - RSpec test suite
46
+ - Development tools and scripts
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Noreaster Group, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # ActiveRecordPrettyKey
2
+
3
+ A Ruby gem for generating and managing pretty, human-readable keys in ActiveRecord models.
4
+
5
+ The gem automatically generates pretty keys by creating unique integer "tickets" and encoding them with the [Sqids](https://github.com/sqids/sqids-ruby) library to produce short, URL-safe string identifiers. Keys are generated before saving records, ensuring both uniqueness and consistency.
6
+
7
+ **Examples:** `BnJe`, `xK4m`, `Qp7R`, `vN2f8` — short, memorable IDs perfect for URLs like `/posts/xK4m`
8
+
9
+ Inspired by:
10
+ - [Flickr Ticket Servers: Distributed Unique Primary Keys on the Cheap](https://code.flickr.net/2010/02/08/ticket-servers-distributed-unique-primary-keys-on-the-cheap/)
11
+ - [Hashids](https://github.com/peterhellberg/hashids.rb)
12
+
13
+ ## Why Pretty Keys?
14
+
15
+ | Feature | Regular Primary Keys | UUID7 | Pretty Keys |
16
+ |---------|---------------------|-------|-------------|
17
+ | **Human Readable** | ✅ Sequential numbers (1, 2, 3...) | ❌ Long, random strings | ✅ Short, memorable strings |
18
+ | **In Order** | ✅ Yes | ✅ Yes | ❌ No |
19
+ | **URL Friendly** | ✅ Simple and clean | ❌ Long and unwieldy | ✅ Short and clean |
20
+ | **Guessability** | ❌ Sequential, easily enumerated | ✅ Hard to guess | ⚠️ Obfuscated, not secret (see [Security](#security)) |
21
+ | **Length** | ✅ Very short | ❌ Very long (36 chars) | ✅ Short (4+ chars, grows slowly) |
22
+ | **Collision Risk** | ✅ None (auto-increment) | ✅ Extremely low | ✅ None (ticket database table) |
23
+
24
+ Pretty keys offer a middle ground between auto-incrementing integers and UUIDs — short, human-readable identifiers that don't expose your raw row IDs.
25
+
26
+ ## Requirements
27
+
28
+ - Ruby 3.0+
29
+ - Rails / ActiveRecord 6.1 – 8.x
30
+ - PostgreSQL (incl. PostGIS), MySQL 8.0+ (`mysql2` or `trilogy` adapter) or SQLite. Other databases raise `ActiveRecordPrettyKey::Error`.
31
+
32
+ ## Installation
33
+
34
+ Add this line to your application's Gemfile:
35
+
36
+ ```ruby
37
+ gem 'active_record_pretty_key'
38
+ ```
39
+
40
+ And then execute:
41
+
42
+ ```bash
43
+ $ bundle install
44
+ ```
45
+
46
+ Or install it yourself as:
47
+
48
+ ```bash
49
+ $ gem install active_record_pretty_key
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ### Setup
55
+
56
+ First, run the generator to create the required tickets table:
57
+
58
+ ```bash
59
+ rails generate active_record_pretty_key:install
60
+ ```
61
+
62
+ This creates a `create_pretty_key_tickets` migration for the single-row `pretty_key_tickets` table that hands out unique ticket numbers. Run it with `rails db:migrate`.
63
+
64
+ ### Customizing or Randomizing the Sqids Alphabet
65
+
66
+ A custom Sqids alphabet can be configured in Rails credentials:
67
+
68
+ ```yaml
69
+ sqids:
70
+ alphabet: "ENwUpVBslGq25afc6i0hyD4tnjxRY31Wouz7HFTeMKgSbmPZvJ9ALXCQdrkI8O"
71
+ ```
72
+
73
+ This can be used to configure a randomized alphabet to obfuscate object IDs. The alphabet is read from credentials the first time a key is generated.
74
+
75
+ > **Warning:** Choose the alphabet before you create any records and never change it afterwards. Existing keys are stored as strings, so a new alphabet can encode a new ticket number to a key that already exists, causing primary key collisions.
76
+
77
+ To generate a shuffled alphabet:
78
+ ```ruby
79
+ require 'sqids'
80
+ puts Sqids::DEFAULT_ALPHABET.split('').shuffle.join
81
+ ```
82
+
83
+ ### Creating Models
84
+
85
+ Pretty keys require that your model use a string primary key:
86
+ ```ruby
87
+ class CreatePosts < ActiveRecord::Migration[8.0]
88
+ def change
89
+ create_table :posts, id: :string do |t|
90
+ t.string :title
91
+
92
+ t.timestamps
93
+ end
94
+ end
95
+ end
96
+ ```
97
+
98
+ You may choose to configure ActiveRecord to always generate migrations with a string primary key:
99
+ ```ruby
100
+ # application.rb
101
+
102
+ # Default string primary key in migrations for use with Sqids
103
+ config.generators do |generate|
104
+ generate.orm :active_record, primary_key_type: :string
105
+ end
106
+ ```
107
+
108
+ ### Including the Concern
109
+
110
+ Include the concern in your ApplicationRecord to use everywhere:
111
+ ```ruby
112
+ class ApplicationRecord < ActiveRecord::Base
113
+ primary_abstract_class
114
+ include ActiveRecordPrettyKey::Concern
115
+ end
116
+ ```
117
+
118
+ OR
119
+
120
+ Include the concern on a model by model basis:
121
+ ```ruby
122
+ class Post < ApplicationRecord
123
+ include ActiveRecordPrettyKey::Concern
124
+ end
125
+ ```
126
+
127
+ ### Generating Keys
128
+
129
+ Keys are generated automatically before a record is created:
130
+ ```ruby
131
+ post = Post.create(title: "My Awesome Post")
132
+ post.id
133
+ # => "BnJe"
134
+ ```
135
+
136
+ Or the generation helper can be called manually. It returns the record's key, and does not replace a key that is already set:
137
+ ```ruby
138
+ post = Post.new(title: "My Awesome Post")
139
+ post.generate_pretty_key
140
+ # => "9LW9"
141
+
142
+ post.save!
143
+ ```
144
+
145
+ Because keys are generated in a `before_create` callback, which runs *after* validation, don't add `validates :id, presence: true`. If you need the key during validation, generate it first:
146
+ ```ruby
147
+ class Post < ApplicationRecord
148
+ include ActiveRecordPrettyKey::Concern
149
+ before_validation :generate_pretty_key, on: :create
150
+ end
151
+ ```
152
+
153
+ ### Associations
154
+
155
+ Every column that refers to a pretty-keyed record must also be a string, including foreign keys and polymorphic `*_id` columns:
156
+ ```ruby
157
+ create_table :comments, id: :string do |t|
158
+ t.references :post, type: :string, foreign_key: true
159
+ t.references :commentable, polymorphic: true, type: :string
160
+ end
161
+ ```
162
+
163
+ ### Active Storage
164
+
165
+ For the same reason, Active Storage's polymorphic `record_id` column must be a string:
166
+ ```ruby
167
+ class FixAttachmentsFks < ActiveRecord::Migration[8.0]
168
+ def change
169
+ change_column :active_storage_attachments, :record_id, :string, null: false
170
+ end
171
+ end
172
+ ```
173
+
174
+ ### How tickets are generated
175
+
176
+ Keys are generated inside the record's save transaction. A ticket number is never handed out twice, even if that transaction rolls back, and generating one doesn't hold locks until it commits:
177
+
178
+ - **PostgreSQL** uses a sequence (`nextval`), which takes no locks and is not rolled back.
179
+ - **MySQL** uses a dedicated connection pool for the tickets table, so each ticket commits immediately on its own, separately from your transaction. The pool is created on first use with the same settings as your application's pool, so each process may open up to `pool` additional database connections; account for this in your database's `max_connections`.
180
+ - **SQLite** allows only one writer at a time, so tickets are generated in your transaction and **rolled back with it**. A key that was read inside a transaction that then rolled back (for example, passed to a job or API before the rollback) can be given to a later record. Use keys outside a transaction only once it has committed.
181
+
182
+ If the ticket row is missing, for example in a database created from `schema.rb` with `db:schema:load` or `db:prepare`, it is recreated automatically.
183
+
184
+ ## Security
185
+
186
+ Pretty keys are **obfuscated, not encrypted**. Each key is an encoded, globally sequential ticket number:
187
+
188
+ - With the default alphabet, anyone can decode a key back to its ticket number (e.g. `Sqids.new(min_length: 4).decode("gbHJ") # => [2]`) and generate the keys before and after it.
189
+ - A shuffled alphabet makes this harder, but it is not a cryptographic secret and should not be treated as one.
190
+ - Because the ticket counter is shared across all models, decoded keys reveal roughly how many records your application has created and how quickly.
191
+
192
+ Never rely on a key being unguessable for authorization. Always check that the current user is allowed to access the record.
193
+
194
+ ## Development
195
+
196
+ After checking out the repo, run `bin/setup` to install dependencies and run the specs. Run `bundle exec rspec` to run the tests, and `bin/console` for an interactive prompt with an in-memory database and a `Post` model.
197
+
198
+ The specs use in-memory SQLite by default. To run them against another database, install its driver group and set `DB` (and optionally `DATABASE_URL`):
199
+
200
+ ```bash
201
+ docker run -d --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=pretty_key_test postgres:17
202
+ BUNDLE_WITH=postgresql bundle install
203
+ DB=postgresql bundle exec rspec
204
+
205
+ docker run -d --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=pretty_key_test mysql:8.4
206
+ BUNDLE_WITH=mysql bundle install
207
+ DB=mysql2 bundle exec rspec # or DB=trilogy
208
+ ```
209
+
210
+ CI runs the suite against all four adapters.
211
+
212
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in the `VERSION` file, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). Releases require RubyGems multi-factor authentication.
213
+
214
+ ## Contributing
215
+
216
+ Bug reports and pull requests are welcome on GitHub at https://github.com/noreastergroup/active_record_pretty_key. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/noreastergroup/active_record_pretty_key/blob/main/CODE_OF_CONDUCT.md).
217
+
218
+ ## License
219
+
220
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
221
+
222
+ ## Code of Conduct
223
+
224
+ Everyone interacting in the ActiveRecordPrettyKey project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/noreastergroup/active_record_pretty_key/blob/main/CODE_OF_CONDUCT.md).
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.2.0
@@ -8,16 +8,15 @@ module ActiveRecordPrettyKey
8
8
  before_create :generate_pretty_key
9
9
  end
10
10
 
11
- private
12
-
11
+ # Assigns a pretty key to the primary key if it is a string column and
12
+ # not already set. Returns the primary key value, or nil when the model's
13
+ # primary key is not a single string column.
13
14
  def generate_pretty_key
14
- pk_attribute = self.class.primary_key
15
- return if pk_attribute.nil? || self.class.attribute_types[pk_attribute].nil? || self.class.attribute_types[pk_attribute].type != :string
16
-
17
- # Ensure PrettyKeyTicket is available
18
- return unless defined?(ActiveRecordPrettyKey::PrettyKeyTicket)
15
+ pk = self.class.primary_key
16
+ return unless pk.is_a?(String) && self.class.type_for_attribute(pk).type == :string
19
17
 
20
- self.assign_attributes(pk_attribute => ActiveRecordPrettyKey::PrettyKeyTicket.next_sqid) unless self.attributes[pk_attribute].present?
18
+ self[pk] = ActiveRecordPrettyKey::PrettyKeyTicket.next_sqid if self[pk].blank?
19
+ self[pk]
21
20
  end
22
21
  end
23
22
  end
@@ -1,43 +1,137 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/map"
4
+ require "concurrent/atomic/atomic_fixnum"
5
+
3
6
  module ActiveRecordPrettyKey
4
7
  class PrettyKeyTicket < ActiveRecord::Base
5
8
  self.table_name = "pretty_key_tickets"
6
- @sqids = Sqids.new(
7
- min_length: 4,
8
- alphabet: defined?(Rails) && Rails.application.respond_to?(:credentials) ?
9
- (Rails.application.credentials.dig(:sqids, :alphabet) || Sqids::DEFAULT_ALPHABET) :
10
- Sqids::DEFAULT_ALPHABET,
11
- )
12
-
13
- # Generate a new unique ID that works with PostgreSQL, MySQL, and SQLite
14
- def self.next_id
15
- transaction do
16
- # Detect database type
17
- adapter = connection.adapter_name.downcase
18
-
19
- if [ "postgresql", "postgis" ].include?(adapter)
20
- # PostgreSQL approach
21
- result = connection.execute(
22
- "INSERT INTO pretty_key_tickets (stub) VALUES ('a')
23
- ON CONFLICT (stub) DO UPDATE SET id = currval('pretty_key_tickets_id_seq')
24
- RETURNING id"
25
- )
26
- result[0]["id"]
27
- elsif [ "mysql", "mysql2" ].include?(adapter)
28
- # MySQL approach
29
- connection.execute("REPLACE INTO pretty_key_tickets (stub) VALUES ('a');")
30
- connection.execute("SELECT LAST_INSERT_ID()").first[0]
31
- else
32
- # SQLite approach (and fallback for others)
33
- connection.execute("REPLACE INTO pretty_key_tickets (stub) VALUES ('a');")
34
- last.id
9
+
10
+ MIN_LENGTH = 4
11
+ MYSQL_DEADLOCK_ATTEMPTS = 5
12
+
13
+ @ticket_connection_pools = Concurrent::Map.new
14
+ @ticket_connection_count = Concurrent::AtomicFixnum.new
15
+
16
+ class << self
17
+ # Generate a new unique integer ID.
18
+ #
19
+ # This is usually called inside the caller's transaction (a model's
20
+ # save). An issued ID must never be handed out again, even if that
21
+ # transaction rolls back, and allocation must not hold locks until the
22
+ # caller commits:
23
+ # - PostgreSQL: nextval() is non-transactional and takes no row locks.
24
+ # - MySQL: UPDATE ... SET id = LAST_INSERT_ID(id + 1), the sequence
25
+ # emulation pattern from the MySQL manual. (Flickr's REPLACE INTO relies
26
+ # on MyISAM; on InnoDB concurrent REPLACEs deadlock.) An UPDATE is
27
+ # transactional, so it runs on a dedicated autocommit connection: it
28
+ # commits immediately and its row lock is released straight away.
29
+ # - SQLite: REPLACE + last_insert_rowid(). SQLite allows a single writer,
30
+ # so a second connection would deadlock with the caller's transaction;
31
+ # the ticket is rolled back along with the caller's transaction.
32
+ #
33
+ # Every query bypasses the Rails query cache: a cached
34
+ # `SELECT nextval(...)` would hand out the same ID twice.
35
+ #
36
+ # Connections are borrowed with connection_pool.with_connection, which
37
+ # reuses the thread's current connection (and so its transaction).
38
+ # Calling `connection` instead would, on Rails 7.2+, keep the connection
39
+ # checked out by the thread after the caller is done with it.
40
+ def next_id
41
+ id = connection_pool.with_connection do |conn|
42
+ case conn.adapter_name.downcase
43
+ when "postgresql", "postgis"
44
+ conn.uncached { conn.select_value("SELECT nextval(#{conn.quote(sequence_name)})") }
45
+ when "mysql", "mysql2", "trilogy"
46
+ ticket_connection_pool.with_connection { |ticket_conn| ticket_conn.uncached { mysql_next_id(ticket_conn) } }
47
+ when "sqlite", "sqlite3"
48
+ conn.uncached do
49
+ conn.execute("REPLACE INTO #{quoted_table_name} (stub) VALUES ('a')")
50
+ conn.select_value("SELECT last_insert_rowid()")
51
+ end
52
+ else
53
+ raise ActiveRecordPrettyKey::Error,
54
+ "Unsupported database adapter #{conn.adapter_name.inspect}; " \
55
+ "supported adapters are PostgreSQL, PostGIS, MySQL (mysql2/trilogy) and SQLite"
56
+ end
35
57
  end
58
+
59
+ Integer(id)
60
+ end
61
+
62
+ # Connection pool used only for MySQL ticket allocation, separate from
63
+ # the application's pool so allocations commit independently of the
64
+ # caller's transaction. One pool per database config (so multiple
65
+ # databases and shards each get their own), created on first use with
66
+ # the same settings, including pool size, as the application's pool.
67
+ # Registered through establish_connection so Rails manages it (e.g.
68
+ # discarding it after fork); the pool object is used directly so it
69
+ # works regardless of the caller's connected_to role or shard.
70
+ def ticket_connection_pool
71
+ db_config = connection_db_config
72
+ @ticket_connection_pools.compute_if_absent(db_config.configuration_hash) do
73
+ # establish_connection needs a named class to own the pool
74
+ owner = Class.new(ActiveRecord::Base) { self.abstract_class = true }
75
+ ActiveRecordPrettyKey.const_set("TicketConnection#{@ticket_connection_count.increment}", owner)
76
+ owner.establish_connection(db_config)
77
+ end
78
+ end
79
+
80
+ def next_sqid
81
+ sqids.encode([ next_id ])
82
+ end
83
+
84
+ # Built lazily so Rails credentials are available. The gem is loaded by
85
+ # Bundler.require before Rails.application exists, so reading credentials
86
+ # at class-load time would silently fall back to the default alphabet.
87
+ def sqids
88
+ @sqids ||= Sqids.new(min_length: MIN_LENGTH, alphabet: alphabet)
36
89
  end
37
- end
38
90
 
39
- def self.next_sqid
40
- @sqids.encode([ self.next_id ])
91
+ def alphabet
92
+ configured =
93
+ if defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
94
+ ::Rails.application.credentials.dig(:sqids, :alphabet)
95
+ end
96
+
97
+ configured.presence || Sqids::DEFAULT_ALPHABET
98
+ end
99
+
100
+ private
101
+
102
+ def mysql_next_id(conn)
103
+ attempts = 0
104
+ begin
105
+ attempts += 1
106
+ mysql_allocate(conn)
107
+ rescue ActiveRecord::Deadlocked
108
+ # Each statement runs in its own autocommit transaction, so the
109
+ # deadlock victim issued nothing and can safely be retried. (Not if
110
+ # something, e.g. a test harness, opened a transaction around it.)
111
+ retry if attempts < MYSQL_DEADLOCK_ATTEMPTS && !conn.transaction_open?
112
+ raise
113
+ end
114
+ end
115
+
116
+ def mysql_allocate(conn)
117
+ update = "UPDATE #{quoted_table_name} SET id = LAST_INSERT_ID(id + 1) WHERE stub = 'a'"
118
+
119
+ if conn.update(update).zero?
120
+ # The row seeded by the install migration is missing, e.g. in a
121
+ # database built from schema.rb (db:schema:load, db:prepare, test
122
+ # databases). Create it; IGNORE covers a concurrent insert. MySQL 8
123
+ # moves the AUTO_INCREMENT counter past values set by UPDATE, so the
124
+ # new row can't start below IDs that were already issued. Concurrent
125
+ # INSERT IGNORE and UPDATE can deadlock here; the caller retries.
126
+ conn.execute("INSERT IGNORE INTO #{quoted_table_name} (stub) VALUES ('a')")
127
+
128
+ if conn.update(update).zero?
129
+ raise ActiveRecordPrettyKey::Error, "Could not create the ticket row in #{table_name}"
130
+ end
131
+ end
132
+
133
+ conn.select_value("SELECT LAST_INSERT_ID()")
134
+ end
41
135
  end
42
136
  end
43
137
  end
@@ -2,12 +2,18 @@
2
2
 
3
3
  require "active_record"
4
4
  require "active_support/concern"
5
+ require "active_support/lazy_load_hooks"
5
6
  require "sqids"
6
- require_relative "active_record_pretty_key/pretty_key_ticket"
7
+ require_relative "active_record_pretty_key/version"
7
8
  require_relative "active_record_pretty_key/concern"
8
- # require_relative "active_record_pretty_key/version"
9
-
10
9
 
11
10
  module ActiveRecordPrettyKey
12
11
  class Error < StandardError; end
13
12
  end
13
+
14
+ # PrettyKeyTicket subclasses ActiveRecord::Base. Defining it when the gem is
15
+ # required would load ActiveRecord::Base before the application's
16
+ # config.active_record settings are applied, so wait for ActiveRecord to load.
17
+ ActiveSupport.on_load(:active_record) do
18
+ require_relative "active_record_pretty_key/pretty_key_ticket"
19
+ end
@@ -1,23 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators"
4
+ require "rails/generators/active_record"
4
5
 
5
6
  module ActiveRecordPrettyKey
6
7
  module Generators
7
8
  class InstallGenerator < Rails::Generators::Base
9
+ include ActiveRecord::Generators::Migration
10
+
8
11
  source_root File.expand_path("templates", __dir__)
9
- desc "Creates the tickets table migration for active_record_pretty_key"
12
+ desc "Creates the pretty_key_tickets table migration for active_record_pretty_key"
10
13
 
11
- def create_migration
12
- template "create_tickets.erb", "db/migrate/#{Time.now.utc.strftime("%Y%m%d%H%M%S")}_create_pretty_key_tickets.rb"
14
+ def create_migration_file
15
+ migration_template "create_pretty_key_tickets.rb.erb",
16
+ File.join(db_migrate_path, "create_pretty_key_tickets.rb")
13
17
  end
14
18
 
15
19
  private
16
20
 
17
21
  def migration_version
18
- if ActiveRecord::VERSION::MAJOR >= 5
19
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
20
- end
22
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
21
23
  end
22
24
  end
23
25
  end
@@ -5,7 +5,9 @@ class CreatePrettyKeyTickets < ActiveRecord::Migration<%= migration_version %>
5
5
  end
6
6
  add_index :pretty_key_tickets, :stub, unique: true
7
7
 
8
- # Seed initial value
9
- execute "INSERT INTO pretty_key_tickets (stub) VALUES ('a')"
8
+ # Seed the single ticket row (dropped along with the table on rollback)
9
+ up_only do
10
+ execute "INSERT INTO pretty_key_tickets (stub) VALUES ('a')"
11
+ end
10
12
  end
11
13
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_pretty_key
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stuart Yamartino
@@ -16,28 +16,40 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 6.0.0
19
+ version: '6.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '9'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
- version: 6.0.0
29
+ version: '6.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: activesupport
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
37
  - - ">="
32
38
  - !ruby/object:Gem::Version
33
- version: 6.0.0
39
+ version: '6.1'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '9'
34
43
  type: :runtime
35
44
  prerelease: false
36
45
  version_requirements: !ruby/object:Gem::Requirement
37
46
  requirements:
38
47
  - - ">="
39
48
  - !ruby/object:Gem::Version
40
- version: 6.0.0
49
+ version: '6.1'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '9'
41
53
  - !ruby/object:Gem::Dependency
42
54
  name: sqids
43
55
  requirement: !ruby/object:Gem::Requirement
@@ -52,20 +64,6 @@ dependencies:
52
64
  - - "~>"
53
65
  - !ruby/object:Gem::Version
54
66
  version: '0.1'
55
- - !ruby/object:Gem::Dependency
56
- name: thor
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '1.0'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '1.0'
69
67
  description: ActiveRecordPrettyKey provides functionality to generate and manage pretty,
70
68
  human-readable keys for ActiveRecord models.
71
69
  email:
@@ -75,13 +73,16 @@ executables: []
75
73
  extensions: []
76
74
  extra_rdoc_files: []
77
75
  files:
76
+ - CHANGELOG.md
77
+ - LICENSE
78
+ - README.md
78
79
  - VERSION
79
80
  - lib/active_record_pretty_key.rb
80
81
  - lib/active_record_pretty_key/concern.rb
81
82
  - lib/active_record_pretty_key/pretty_key_ticket.rb
82
83
  - lib/active_record_pretty_key/version.rb
83
84
  - lib/generators/active_record_pretty_key/install/install_generator.rb
84
- - lib/generators/active_record_pretty_key/install/templates/create_tickets.erb
85
+ - lib/generators/active_record_pretty_key/install/templates/create_pretty_key_tickets.rb.erb
85
86
  homepage: https://github.com/noreastergroup/active_record_pretty_key
86
87
  licenses:
87
88
  - MIT
@@ -89,6 +90,8 @@ metadata:
89
90
  homepage_uri: https://github.com/noreastergroup/active_record_pretty_key
90
91
  source_code_uri: https://github.com/noreastergroup/active_record_pretty_key
91
92
  changelog_uri: https://github.com/noreastergroup/active_record_pretty_key/blob/main/CHANGELOG.md
93
+ bug_tracker_uri: https://github.com/noreastergroup/active_record_pretty_key/issues
94
+ rubygems_mfa_required: 'true'
92
95
  rdoc_options: []
93
96
  require_paths:
94
97
  - lib
@@ -96,14 +99,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
96
99
  requirements:
97
100
  - - ">="
98
101
  - !ruby/object:Gem::Version
99
- version: 2.7.0
102
+ version: 3.0.0
100
103
  required_rubygems_version: !ruby/object:Gem::Requirement
101
104
  requirements:
102
105
  - - ">="
103
106
  - !ruby/object:Gem::Version
104
107
  version: '0'
105
108
  requirements: []
106
- rubygems_version: 3.6.7
109
+ rubygems_version: 3.6.9
107
110
  specification_version: 4
108
111
  summary: A gem for generating pretty keys in ActiveRecord models
109
112
  test_files: []