rails-uuid-pk 0.12.0 → 0.13.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/ARCHITECTURE.md +508 -0
- data/CHANGELOG.md +62 -0
- data/DEVELOPMENT.md +314 -0
- data/PERFORMANCE.md +417 -0
- data/README.md +43 -12
- data/SECURITY.md +321 -0
- data/lib/generators/rails_uuid_pk/add_opt_outs_generator.rb +183 -0
- data/lib/rails_uuid_pk/migration_helpers.rb +6 -2
- data/lib/rails_uuid_pk/mysql2_adapter_extension.rb +10 -4
- data/lib/rails_uuid_pk/railtie.rb +7 -2
- data/lib/rails_uuid_pk/trilogy_adapter_extension.rb +39 -0
- data/lib/rails_uuid_pk/version.rb +2 -2
- data/lib/rails_uuid_pk.rb +4 -0
- metadata +25 -3
data/SECURITY.md
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Security Considerations
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
We take security seriously and actively maintain this gem. The following versions are currently supported with security updates:
|
|
6
|
+
|
|
7
|
+
| Version | Supported | Security Support Until |
|
|
8
|
+
| ------- | ------------------ | ---------------------- |
|
|
9
|
+
| 0.13.x | :white_check_mark: | Ongoing |
|
|
10
|
+
| 0.12.x | :white_check_mark: | 6 months from 0.13.0 |
|
|
11
|
+
| 0.11.x | :white_check_mark: | 6 months from 0.12.0 |
|
|
12
|
+
| < 0.11 | :x: | None |
|
|
13
|
+
|
|
14
|
+
## Reporting a Vulnerability
|
|
15
|
+
|
|
16
|
+
**Please report security vulnerabilities responsibly.**
|
|
17
|
+
|
|
18
|
+
If you discover a security vulnerability in rails-uuid-pk, please:
|
|
19
|
+
|
|
20
|
+
1. **DO NOT** create a public GitHub issue
|
|
21
|
+
2. Email security concerns to: **seouri@gmail.com**
|
|
22
|
+
3. Include detailed information about:
|
|
23
|
+
- The vulnerability description
|
|
24
|
+
- Steps to reproduce
|
|
25
|
+
- Potential impact assessment
|
|
26
|
+
- Your contact information for follow-up
|
|
27
|
+
|
|
28
|
+
**Response Time**: We will acknowledge receipt within 24 hours and provide a more detailed response within 72 hours indicating our next steps.
|
|
29
|
+
|
|
30
|
+
## Cryptographic Security Analysis
|
|
31
|
+
|
|
32
|
+
### UUIDv7 Cryptographic Properties
|
|
33
|
+
|
|
34
|
+
This gem uses **UUIDv7** (RFC 9562) for primary key generation, which provides the following security characteristics:
|
|
35
|
+
|
|
36
|
+
> **Privacy Consideration**: UUIDv7 includes a timestamp component that reveals approximate record creation time. Do not use if creation timestamp must be hidden.
|
|
37
|
+
>
|
|
38
|
+
> **Enhanced Privacy Documentation**: Added explicit warning about UUIDv7 timestamp exposure in SECURITY.md, clarifying that UUIDv7 includes a timestamp component that reveals approximate record creation time and advising against use when creation timestamps must be hidden.
|
|
39
|
+
|
|
40
|
+
#### Strengths
|
|
41
|
+
- **Cryptographically Secure Generation**: Uses Ruby's `SecureRandom.uuid_v7()` backed by system CSPRNG (OpenSSL or system entropy)
|
|
42
|
+
- **Monotonic Ordering**: Time-based ordering prevents index fragmentation while maintaining unpredictability
|
|
43
|
+
- **High Entropy**: 128-bit randomness with structured time component
|
|
44
|
+
- **RFC 9562 Compliance**: Follows latest UUID standards
|
|
45
|
+
|
|
46
|
+
#### Known Limitations
|
|
47
|
+
- **Timestamp Exposure**: First 48 bits contain millisecond-precision timestamp
|
|
48
|
+
- **Predictability Window**: Generated UUIDs reveal creation time ± milliseconds
|
|
49
|
+
- **No Forward Secrecy**: Compromised keys don't affect future UUID security
|
|
50
|
+
|
|
51
|
+
### Timestamp Exposure Considerations
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# Example: UUIDv7 reveals generation timestamp
|
|
55
|
+
uuid = "017f22e2-79b0-7cc3-98c4-dc0c0c07398f"
|
|
56
|
+
timestamp_ms = uuid[0..7].to_i(16) >> 4 # Extract 48-bit timestamp
|
|
57
|
+
# This reveals the UUID was generated at: 2023-11-15 10:30:45.123 UTC
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Security Impact**: An attacker observing UUIDs can determine:
|
|
61
|
+
- Approximate creation time of records
|
|
62
|
+
- Rate of record generation
|
|
63
|
+
- Potential correlation between UUID sequences and business activities
|
|
64
|
+
|
|
65
|
+
**Mitigations**:
|
|
66
|
+
- Use UUIDv4 for applications requiring maximum unpredictability
|
|
67
|
+
- Implement rate limiting to prevent timing attacks
|
|
68
|
+
- Avoid exposing UUIDs in public APIs when timing data is sensitive
|
|
69
|
+
|
|
70
|
+
## Database Security Implications
|
|
71
|
+
|
|
72
|
+
### Primary Key Security
|
|
73
|
+
|
|
74
|
+
#### Sequential ID Vulnerabilities (Avoided)
|
|
75
|
+
Traditional auto-incrementing IDs create security risks:
|
|
76
|
+
- **Enumeration Attacks**: `SELECT * FROM users WHERE id > 1000` reveals user count
|
|
77
|
+
- **Resource Discovery**: Predictable URLs enable scraping
|
|
78
|
+
- **Race Conditions**: Concurrent requests can leak information
|
|
79
|
+
|
|
80
|
+
#### UUIDv7 Advantages
|
|
81
|
+
- **Non-Enumerability**: No predictable sequence for attackers to exploit
|
|
82
|
+
- **Global Uniqueness**: No collision risks across distributed systems
|
|
83
|
+
- **Index Efficiency**: Time-ordered UUIDs provide better B-tree performance
|
|
84
|
+
|
|
85
|
+
### Foreign Key Security Considerations
|
|
86
|
+
|
|
87
|
+
#### Polymorphic Associations
|
|
88
|
+
When using polymorphic references with UUID primary keys:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
# Migration
|
|
92
|
+
create_table :comments do |t|
|
|
93
|
+
t.references :commentable, polymorphic: true, type: :uuid
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Security Implications**:
|
|
98
|
+
- Foreign keys become non-guessable
|
|
99
|
+
- Prevents enumeration of related records
|
|
100
|
+
- Complicates unauthorized data access patterns
|
|
101
|
+
|
|
102
|
+
#### Join Table Exposure
|
|
103
|
+
Many-to-many relationships expose UUIDs in join tables:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
# users_posts join table contains UUID foreign keys
|
|
107
|
+
# An attacker seeing these can correlate users with content
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Recommendations**:
|
|
111
|
+
- Use appropriate access controls regardless of key type
|
|
112
|
+
- Implement row-level security when needed
|
|
113
|
+
- Consider UUID visibility in audit logs
|
|
114
|
+
|
|
115
|
+
## Performance-Security Trade-offs
|
|
116
|
+
|
|
117
|
+
### Index Performance vs Security
|
|
118
|
+
|
|
119
|
+
| Aspect | Integer Keys | UUIDv7 Keys | Security Impact |
|
|
120
|
+
|--------|-------------|-------------|-----------------|
|
|
121
|
+
| **Index Size** | 4 bytes | 16 bytes | Neutral |
|
|
122
|
+
| **Cache Efficiency** | Excellent | Good | Neutral |
|
|
123
|
+
| **Predictability** | High Risk | Low Risk | Security Benefit |
|
|
124
|
+
| **Fragmentation** | None | Low (monotonic) | Security Benefit |
|
|
125
|
+
|
|
126
|
+
### Query Performance Considerations
|
|
127
|
+
|
|
128
|
+
#### Range Queries on Time
|
|
129
|
+
UUIDv7's time-based ordering enables efficient time-range queries:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# Find records from last hour (efficient with UUIDv7)
|
|
133
|
+
User.where("id >= ? AND id < ?", min_uuid_for_hour, max_uuid_for_hour)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Security Benefit**: Enables efficient audit logging and temporal access controls
|
|
137
|
+
|
|
138
|
+
#### Index Bloat Monitoring
|
|
139
|
+
Large UUID indexes may require monitoring:
|
|
140
|
+
|
|
141
|
+
```sql
|
|
142
|
+
-- Monitor index bloat (PostgreSQL example)
|
|
143
|
+
SELECT schemaname, tablename, attname, n_distinct, correlation
|
|
144
|
+
FROM pg_stats
|
|
145
|
+
WHERE tablename = 'users' AND attname = 'id';
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Recommendation**: Monitor index statistics and plan maintenance windows
|
|
149
|
+
|
|
150
|
+
## Side-Channel Attack Vectors
|
|
151
|
+
|
|
152
|
+
### Timing Attacks
|
|
153
|
+
|
|
154
|
+
#### UUID Generation Timing
|
|
155
|
+
UUID generation is fast and constant-time, but bulk operations may reveal system load:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
# Bulk UUID generation timing can reveal system capacity
|
|
159
|
+
start = Time.now
|
|
160
|
+
100.times { User.create!(name: "User") }
|
|
161
|
+
duration = Time.now - start
|
|
162
|
+
# Duration reveals concurrent load and system performance
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Mitigation**: Implement rate limiting and monitoring
|
|
166
|
+
|
|
167
|
+
### Information Leakage Through Errors
|
|
168
|
+
|
|
169
|
+
#### Database Error Messages
|
|
170
|
+
UUID validation errors may leak information:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# Potential information leakage
|
|
174
|
+
User.find("invalid-uuid") # => ActiveRecord::RecordNotFound
|
|
175
|
+
# vs
|
|
176
|
+
User.find("550e8400-e29b-41d4-a716-446655440000") # => User or Not Found
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Mitigation**: Use consistent error messages regardless of UUID validity
|
|
180
|
+
|
|
181
|
+
### Cross-Application Correlation
|
|
182
|
+
|
|
183
|
+
#### UUID Reuse Across Services
|
|
184
|
+
Using the same UUID generation in multiple applications can create correlation vectors:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
# Service A: User UUID
|
|
188
|
+
# Service B: Order UUID with same timestamp
|
|
189
|
+
# Correlation: Same user placed order at same millisecond
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Recommendation**: Use service-specific UUID namespaces or additional entropy
|
|
193
|
+
|
|
194
|
+
## Dependency Security
|
|
195
|
+
|
|
196
|
+
This gem has minimal dependencies with known security postures:
|
|
197
|
+
|
|
198
|
+
### Runtime Dependencies
|
|
199
|
+
- **rails (~> 8.0)**: Monitored via Rails security advisories
|
|
200
|
+
- **Database adapters**: Follow respective project security practices
|
|
201
|
+
- **pg (~> 1.6.3)**: PostgreSQL adapter (compatible with PostgreSQL 18+)
|
|
202
|
+
- **mysql2 (~> 0.5.7)**: MySQL adapter (compatible with MySQL 9+)
|
|
203
|
+
- **sqlite3 (~> 2.9.0)**: SQLite adapter
|
|
204
|
+
|
|
205
|
+
### Development Dependencies
|
|
206
|
+
- Testing frameworks with regular security updates
|
|
207
|
+
- Code quality tools (RuboCop, RuboCop-Rails)
|
|
208
|
+
|
|
209
|
+
## Secure Usage Guidelines
|
|
210
|
+
|
|
211
|
+
### 1. Application-Level Security
|
|
212
|
+
```ruby
|
|
213
|
+
# DO: Use UUIDs for public-facing identifiers
|
|
214
|
+
class Post < ApplicationRecord
|
|
215
|
+
# UUID primary key is secure by default
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# DON'T: Don't rely on UUID secrecy alone
|
|
219
|
+
class User < ApplicationRecord
|
|
220
|
+
# Still need authentication and authorization
|
|
221
|
+
def visible_posts
|
|
222
|
+
posts.where(published: true) # Business logic access control
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### 2. API Security
|
|
228
|
+
```ruby
|
|
229
|
+
# DO: Use UUIDs in APIs
|
|
230
|
+
get '/posts/:id' do
|
|
231
|
+
post = Post.find_by!(id: params[:id])
|
|
232
|
+
authorize! :read, post # Authorization still required
|
|
233
|
+
render post
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# DON'T: Don't assume UUIDs prevent all attacks
|
|
237
|
+
# Rate limiting, input validation still essential
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 3. Database Security
|
|
241
|
+
```ruby
|
|
242
|
+
# DO: Use appropriate access controls
|
|
243
|
+
class ApplicationPolicy
|
|
244
|
+
def show?
|
|
245
|
+
user.admin? || record.user_id == user.id
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# DON'T: Don't expose UUIDs unnecessarily
|
|
250
|
+
# Consider using different identifiers for public APIs
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Security Testing Recommendations
|
|
254
|
+
|
|
255
|
+
### Automated Security Testing
|
|
256
|
+
```ruby
|
|
257
|
+
# Add to test suite
|
|
258
|
+
class SecurityTest < ActiveSupport::TestCase
|
|
259
|
+
test "UUIDs are not predictable" do
|
|
260
|
+
uuids = 1000.times.map { SecureRandom.uuid_v7 }
|
|
261
|
+
# Verify no obvious patterns
|
|
262
|
+
assert uuids.uniq.length == uuids.length
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
test "timing attack resistance" do
|
|
266
|
+
# Verify constant-time UUID generation
|
|
267
|
+
times = 100.times.map do
|
|
268
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
269
|
+
SecureRandom.uuid_v7
|
|
270
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
variance = times.max - times.min
|
|
274
|
+
assert variance < 0.001 # Less than 1ms variance
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Penetration Testing Checklist
|
|
280
|
+
- [ ] UUID enumeration attempts
|
|
281
|
+
- [ ] Timing attack analysis
|
|
282
|
+
- [ ] Information leakage through errors
|
|
283
|
+
- [ ] Cross-service correlation analysis
|
|
284
|
+
- [ ] Database access pattern analysis
|
|
285
|
+
|
|
286
|
+
## Security Maintenance
|
|
287
|
+
|
|
288
|
+
### Regular Security Reviews
|
|
289
|
+
- **Monthly**: Dependency updates and vulnerability scans
|
|
290
|
+
- **Quarterly**: Security architecture review
|
|
291
|
+
- **Annually**: Full security assessment
|
|
292
|
+
|
|
293
|
+
### Security Monitoring
|
|
294
|
+
- Monitor for unusual UUID generation patterns
|
|
295
|
+
- Alert on potential enumeration attacks
|
|
296
|
+
- Track performance degradation that might indicate attacks
|
|
297
|
+
|
|
298
|
+
## Compliance Considerations
|
|
299
|
+
|
|
300
|
+
### GDPR and Privacy
|
|
301
|
+
- UUIDs as personal data: Generally not considered personal information
|
|
302
|
+
- Audit logging: UUIDs provide excellent audit trails
|
|
303
|
+
- Data minimization: Consider if UUID exposure meets data minimization requirements
|
|
304
|
+
|
|
305
|
+
### Industry-Specific Security
|
|
306
|
+
- **Healthcare (HIPAA)**: UUIDs support proper access controls
|
|
307
|
+
- **Financial Services**: Enhanced audit capabilities
|
|
308
|
+
- **Government**: Supports classification and access controls
|
|
309
|
+
|
|
310
|
+
## Conclusion
|
|
311
|
+
|
|
312
|
+
Rails-UUID-PK provides a secure foundation for UUIDv7 primary keys with proper cryptographic properties and database security benefits. However, security is defense-in-depth: UUIDs enhance security but don't replace proper authentication, authorization, and access controls.
|
|
313
|
+
|
|
314
|
+
**Key Takeaways**:
|
|
315
|
+
1. UUIDv7 provides better security than sequential IDs
|
|
316
|
+
2. Timestamp exposure is a known trade-off
|
|
317
|
+
3. Application-level security controls remain essential
|
|
318
|
+
4. Monitor performance and security metrics in production
|
|
319
|
+
5. Regular security reviews and updates are critical
|
|
320
|
+
|
|
321
|
+
For questions about security or to report vulnerabilities, contact the security team at seouri@gmail.com.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
require "rails/generators/base"
|
|
2
|
+
|
|
3
|
+
module RailsUuidPk
|
|
4
|
+
# Rails generators for the rails-uuid-pk gem.
|
|
5
|
+
#
|
|
6
|
+
# This module contains Rails generator classes that help with migration
|
|
7
|
+
# and setup tasks for applications using UUID primary keys.
|
|
8
|
+
#
|
|
9
|
+
# @see RailsUuidPk::Generators::AddOptOutsGenerator
|
|
10
|
+
module Generators
|
|
11
|
+
# Rails generator that scans all ActiveRecord models in a Rails application
|
|
12
|
+
# and adds `use_integer_primary_key` to models that have integer primary keys
|
|
13
|
+
# in the database schema.
|
|
14
|
+
#
|
|
15
|
+
# This generator helps migrate existing Rails applications that use integer
|
|
16
|
+
# primary keys to work correctly with the rails-uuid-pk gem, which assumes
|
|
17
|
+
# UUID primary keys by default.
|
|
18
|
+
#
|
|
19
|
+
# @example Run the generator
|
|
20
|
+
# rails generate rails_uuid_pk:add_opt_outs
|
|
21
|
+
#
|
|
22
|
+
# @example Run with dry-run to see what would be changed
|
|
23
|
+
# rails generate rails_uuid_pk:add_opt_outs --dry-run
|
|
24
|
+
#
|
|
25
|
+
# @see RailsUuidPk::HasUuidv7PrimaryKey
|
|
26
|
+
# @see https://github.com/seouri/rails-uuid-pk
|
|
27
|
+
class AddOptOutsGenerator < Rails::Generators::Base
|
|
28
|
+
include Thor::Actions
|
|
29
|
+
|
|
30
|
+
desc "Scans all ActiveRecord models and adds use_integer_primary_key to models with integer primary keys"
|
|
31
|
+
|
|
32
|
+
class_option :dry_run, type: :boolean, default: false, desc: "Show what would be changed without modifying files"
|
|
33
|
+
class_option :verbose, type: :boolean, default: true, desc: "Provide detailed output for each model processed"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Analyzes all ActiveRecord models and modifies those with integer primary keys.
|
|
38
|
+
#
|
|
39
|
+
# This is the main generator method that orchestrates the entire process:
|
|
40
|
+
# 1. Finds all model files in the application
|
|
41
|
+
# 2. Analyzes each model for integer primary keys
|
|
42
|
+
# 3. Modifies model files to add opt-out calls
|
|
43
|
+
# 4. Reports results to the user
|
|
44
|
+
#
|
|
45
|
+
# @return [void]
|
|
46
|
+
def analyze_and_modify_models
|
|
47
|
+
say_status :info, "Analyzing ActiveRecord models for integer primary keys...", :blue
|
|
48
|
+
|
|
49
|
+
results = analyze_models
|
|
50
|
+
|
|
51
|
+
modified_count = 0
|
|
52
|
+
analyzed_count = results.size
|
|
53
|
+
|
|
54
|
+
results.each do |result|
|
|
55
|
+
if options[:verbose]
|
|
56
|
+
status = case
|
|
57
|
+
when result[:modified] then :modified
|
|
58
|
+
when result[:needs_opt_out] && !result[:already_has_opt_out] then :pending
|
|
59
|
+
else :skipped
|
|
60
|
+
end
|
|
61
|
+
say_status status, "#{result[:model_class].name} (table: #{result[:table_name]}, pk: #{result[:primary_key_type]})", :green
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
modified_count += 1 if result[:modified]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
say_status :success, "Analyzed #{analyzed_count} models, modified #{modified_count} files", :green
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def analyze_models
|
|
73
|
+
model_files = find_model_files
|
|
74
|
+
connection = ActiveRecord::Base.connection
|
|
75
|
+
|
|
76
|
+
model_files.map do |file_path|
|
|
77
|
+
class_name = extract_class_name_from_file(file_path)
|
|
78
|
+
next unless class_name
|
|
79
|
+
|
|
80
|
+
model_class = class_name.constantize
|
|
81
|
+
next unless model_class < ActiveRecord::Base
|
|
82
|
+
|
|
83
|
+
table_name = model_class.table_name
|
|
84
|
+
next unless connection.table_exists?(table_name)
|
|
85
|
+
|
|
86
|
+
primary_key_type = check_primary_key_type(table_name, connection)
|
|
87
|
+
already_has_opt_out = model_file_has_opt_out?(file_path)
|
|
88
|
+
|
|
89
|
+
needs_opt_out = primary_key_type == :integer && !already_has_opt_out
|
|
90
|
+
|
|
91
|
+
modified = false
|
|
92
|
+
if needs_opt_out && !options[:dry_run]
|
|
93
|
+
modified = add_opt_out_to_model(file_path, class_name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
{
|
|
97
|
+
model_class: model_class,
|
|
98
|
+
table_name: table_name,
|
|
99
|
+
primary_key_type: primary_key_type,
|
|
100
|
+
needs_opt_out: needs_opt_out,
|
|
101
|
+
already_has_opt_out: already_has_opt_out,
|
|
102
|
+
file_path: file_path,
|
|
103
|
+
modified: modified
|
|
104
|
+
}
|
|
105
|
+
end.compact
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def find_model_files
|
|
109
|
+
Dir.glob(File.join(destination_root, "app/models/**/*.rb"))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def extract_class_name_from_file(file_path)
|
|
113
|
+
content = File.read(file_path)
|
|
114
|
+
# Simple regex to find class definition - this is a basic implementation
|
|
115
|
+
# In a real scenario, you'd want to use a Ruby parser for accuracy
|
|
116
|
+
match = content.match(/class\s+([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*)/)
|
|
117
|
+
match[1] if match
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def check_primary_key_type(table_name, connection)
|
|
121
|
+
columns = connection.columns(table_name)
|
|
122
|
+
pk_column = columns.find { |col| col.name == "id" }
|
|
123
|
+
return :unknown unless pk_column
|
|
124
|
+
|
|
125
|
+
# Map database types to our categories
|
|
126
|
+
case pk_column.type
|
|
127
|
+
when :integer then :integer
|
|
128
|
+
when :string then pk_column.limit == 36 ? :uuid : :string
|
|
129
|
+
when :uuid then :uuid
|
|
130
|
+
else :unknown
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def model_file_has_opt_out?(file_path)
|
|
135
|
+
content = File.read(file_path)
|
|
136
|
+
content.include?("use_integer_primary_key")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def add_opt_out_to_model(file_path, class_name)
|
|
140
|
+
content = File.read(file_path)
|
|
141
|
+
|
|
142
|
+
# Find the class definition line
|
|
143
|
+
class_match = content.match(/^(\s*)class\s+#{Regexp.escape(class_name)}/)
|
|
144
|
+
return false unless class_match
|
|
145
|
+
|
|
146
|
+
indent = class_match[1]
|
|
147
|
+
|
|
148
|
+
# Find where to insert - after the class definition and any initial comments/constants
|
|
149
|
+
lines = content.lines
|
|
150
|
+
insert_index = nil
|
|
151
|
+
|
|
152
|
+
lines.each_with_index do |line, index|
|
|
153
|
+
if line.match?(/^#{Regexp.escape(indent)}class\s+#{Regexp.escape(class_name)}/)
|
|
154
|
+
# Start looking for insertion point after this line
|
|
155
|
+
(index + 1..lines.size - 1).each do |i|
|
|
156
|
+
current_line = lines[i]
|
|
157
|
+
next if current_line.strip.empty? || current_line.match?(/^#{Regexp.escape(indent)}\s*#/)
|
|
158
|
+
|
|
159
|
+
# Insert before the first indented content or end of class
|
|
160
|
+
if current_line.match?(/^#{Regexp.escape(indent)}\s+\S/) || current_line.match?(/^#{Regexp.escape(indent)}end$/) || i == lines.size - 1
|
|
161
|
+
insert_index = i
|
|
162
|
+
break
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
break
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
return false unless insert_index
|
|
170
|
+
|
|
171
|
+
# Insert the opt-out method call
|
|
172
|
+
opt_out_line = "\n#{indent} use_integer_primary_key\n"
|
|
173
|
+
new_content = lines.insert(insert_index, opt_out_line).join
|
|
174
|
+
|
|
175
|
+
File.write(file_path, new_content)
|
|
176
|
+
true
|
|
177
|
+
rescue => e
|
|
178
|
+
say_status :error, "Failed to modify #{file_path}: #{e.message}", :red
|
|
179
|
+
false
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -190,7 +190,10 @@ module RailsUuidPk
|
|
|
190
190
|
return false unless conn.respond_to?(:table_exists?) && conn.table_exists?(table_name)
|
|
191
191
|
|
|
192
192
|
pk_column = find_primary_key_column(table_name, conn)
|
|
193
|
-
@uuid_pk_cache[table_name] = !!(pk_column && pk_column.sql_type
|
|
193
|
+
@uuid_pk_cache[table_name] = !!(pk_column && pk_column.sql_type&.downcase&.match?(/\A(uuid|varchar\(36\))\z/))
|
|
194
|
+
rescue StandardError
|
|
195
|
+
# Handle database connection errors gracefully
|
|
196
|
+
@uuid_pk_cache[table_name] = false
|
|
194
197
|
end
|
|
195
198
|
|
|
196
199
|
# Finds the primary key column for a given table.
|
|
@@ -200,7 +203,8 @@ module RailsUuidPk
|
|
|
200
203
|
# @return [ActiveRecord::ConnectionAdapters::Column, nil] The primary key column or nil
|
|
201
204
|
def find_primary_key_column(table_name, conn)
|
|
202
205
|
pk_name = conn.primary_key(table_name)
|
|
203
|
-
|
|
206
|
+
# Only consider standard Rails primary keys named 'id' for UUID detection
|
|
207
|
+
return nil unless pk_name == "id"
|
|
204
208
|
|
|
205
209
|
conn.columns(table_name).find { |c| c.name == pk_name }
|
|
206
210
|
end
|
|
@@ -7,10 +7,16 @@ module RailsUuidPk
|
|
|
7
7
|
# type support. It includes the shared UUID adapter extension functionality
|
|
8
8
|
# and provides MySQL-specific connection configuration.
|
|
9
9
|
#
|
|
10
|
-
# @example
|
|
11
|
-
# #
|
|
12
|
-
# create_table :users do |t|
|
|
13
|
-
# t.
|
|
10
|
+
# @example UUID primary key and foreign key references
|
|
11
|
+
# # Primary key uses UUID type
|
|
12
|
+
# create_table :users, id: :uuid do |t|
|
|
13
|
+
# t.string :name
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# # Foreign key automatically detects and uses UUID type
|
|
17
|
+
# create_table :posts do |t|
|
|
18
|
+
# t.references :user # Automatically uses :uuid type
|
|
19
|
+
# t.string :title
|
|
14
20
|
# end
|
|
15
21
|
#
|
|
16
22
|
# @see RailsUuidPk::UuidAdapterExtension
|
|
@@ -33,13 +33,14 @@ module RailsUuidPk
|
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
# Configures type mappings for SQLite and
|
|
36
|
+
# Configures type mappings for SQLite, MySQL, and Trilogy adapters.
|
|
37
37
|
#
|
|
38
38
|
# Registers the custom UUID type for adapters that don't have native UUID support.
|
|
39
|
+
# Supports both mysql2 and trilogy adapters for MySQL.
|
|
39
40
|
initializer "rails-uuid-pk.configure_type_map", after: "active_record.initialize_database" do
|
|
40
41
|
ActiveSupport.on_load(:active_record) do
|
|
41
42
|
adapter_name = ActiveRecord::Base.connection.adapter_name
|
|
42
|
-
if %w[SQLite MySQL].include?(adapter_name)
|
|
43
|
+
if %w[SQLite MySQL Trilogy].include?(adapter_name)
|
|
43
44
|
RailsUuidPk::Railtie.register_uuid_type(adapter_name.downcase.to_sym)
|
|
44
45
|
end
|
|
45
46
|
end
|
|
@@ -57,6 +58,10 @@ module RailsUuidPk
|
|
|
57
58
|
ActiveSupport.on_load(:active_record_mysql2adapter) do
|
|
58
59
|
prepend RailsUuidPk::Mysql2AdapterExtension
|
|
59
60
|
end
|
|
61
|
+
|
|
62
|
+
ActiveSupport.on_load(:active_record_trilogyadapter) do
|
|
63
|
+
prepend RailsUuidPk::TrilogyAdapterExtension
|
|
64
|
+
end
|
|
60
65
|
end
|
|
61
66
|
|
|
62
67
|
# Ensures UUID types are registered on all database connections.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsUuidPk
|
|
4
|
+
# Trilogy adapter extension for UUID type support.
|
|
5
|
+
#
|
|
6
|
+
# This module extends ActiveRecord's Trilogy adapter to provide native UUID
|
|
7
|
+
# type support. It includes the shared UUID adapter extension functionality
|
|
8
|
+
# and provides Trilogy-specific connection configuration.
|
|
9
|
+
#
|
|
10
|
+
# Trilogy is GitHub's high-performance MySQL adapter that offers significant
|
|
11
|
+
# performance improvements over the standard mysql2 adapter, particularly for
|
|
12
|
+
# high-traffic Rails applications.
|
|
13
|
+
#
|
|
14
|
+
# @example UUID primary key and foreign key references
|
|
15
|
+
# # Primary key uses UUID type
|
|
16
|
+
# create_table :users, id: :uuid do |t|
|
|
17
|
+
# t.string :name
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# # Foreign key automatically detects and uses UUID type
|
|
21
|
+
# create_table :posts do |t|
|
|
22
|
+
# t.references :user # Automatically uses :uuid type
|
|
23
|
+
# t.string :title
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# @see RailsUuidPk::UuidAdapterExtension
|
|
27
|
+
# @see RailsUuidPk::Type::Uuid
|
|
28
|
+
# @see https://github.com/trilogy-libraries/trilogy
|
|
29
|
+
module TrilogyAdapterExtension
|
|
30
|
+
include RailsUuidPk::UuidAdapterExtension
|
|
31
|
+
|
|
32
|
+
# Configures the database connection with UUID type support.
|
|
33
|
+
#
|
|
34
|
+
# @return [void]
|
|
35
|
+
def configure_connection
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/rails_uuid_pk.rb
CHANGED
|
@@ -6,8 +6,12 @@ require "rails_uuid_pk/type"
|
|
|
6
6
|
require "rails_uuid_pk/uuid_adapter_extension"
|
|
7
7
|
require "rails_uuid_pk/sqlite3_adapter_extension"
|
|
8
8
|
require "rails_uuid_pk/mysql2_adapter_extension"
|
|
9
|
+
require "rails_uuid_pk/trilogy_adapter_extension"
|
|
9
10
|
require "rails_uuid_pk/railtie"
|
|
10
11
|
|
|
12
|
+
# Load generators
|
|
13
|
+
require "generators/rails_uuid_pk/add_opt_outs_generator" if defined?(Rails::Generators)
|
|
14
|
+
|
|
11
15
|
# Rails UUID Primary Key
|
|
12
16
|
#
|
|
13
17
|
# A Rails gem that automatically uses UUIDv7 for all primary keys in Rails applications.
|