Skip to content

Commit b63fbfe

Browse files
committed
Summary: UUID and ULID Primary Key Support
Files Modified insertable.cr - Added compile-time type checking to generate UUID/ULID before insert: For Pk == UUID: Generates UUID.random, stores as String in DB For Pk == String: Generates ULID-compatible ID using CQL::ULIDCompat For Int32/Int64: Uses existing auto-increment behavior identifyable.cr - Modified to handle UUID storage: For UUID models: Uses @_id_storage : String? with @[DB::Field(key: "id")] annotation Exposes id : UUID? while storing as String for DB compatibility Added clear_id! protected method attributes.cr - UUID to String conversion and DB::Field key mapping queryable.cr - Converts UUID to String in find methods deleteable.cr - UUID handling in delete operations persistence.cr - UUID handling for touch/touch_all ulid_compat.cr (new) - ULID-compatible generator using Time.utc (fixes ULID library's deprecated Time.now) cql.cr - Added require for ulid_compat.cr Test Files Created uuid_primary_key_spec.cr - 10 tests for UUID primary keys ulid_primary_key_spec.cr - 12 tests for ULID (String) primary keys Test Results UUID tests: 10 pass ULID tests: 12 pass All active_record tests: 613 pass (4 pending - unrelated join tests)
1 parent 83d5766 commit b63fbfe

File tree

12 files changed

+788
-126
lines changed

12 files changed

+788
-126
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
require "../../spec_helper"
2+
3+
# Schema for ULID primary key tests
4+
UlidTestDB = CQL::Schema.define(
5+
:ulid_test_db,
6+
adapter: CQL::Adapter::SQLite,
7+
uri: "sqlite3:///tmp/ulid_pk_spec.db"
8+
) do
9+
table :ulid_events do
10+
primary :id, String, auto_increment: false # ULID stored as TEXT
11+
text :event_type
12+
text :payload
13+
timestamps
14+
end
15+
end
16+
17+
# Model with ULID (String) primary key
18+
class UlidEvent
19+
include CQL::ActiveRecord::Model(String)
20+
21+
db_context schema: UlidTestDB, table: :ulid_events
22+
23+
property event_type : String = ""
24+
property payload : String = ""
25+
property created_at : Time?
26+
property updated_at : Time?
27+
28+
def initialize
29+
end
30+
31+
def initialize(@event_type : String, @payload : String)
32+
end
33+
end
34+
35+
describe "ULID Primary Key Support" do
36+
before_all do
37+
UlidTestDB.ulid_events.drop! rescue nil
38+
UlidTestDB.ulid_events.create!
39+
end
40+
41+
after_all do
42+
UlidTestDB.ulid_events.drop! rescue nil
43+
end
44+
45+
# Clean up data between tests
46+
before_each do
47+
UlidTestDB.exec("DELETE FROM ulid_events")
48+
end
49+
50+
describe ".create!" do
51+
context "with named arguments" do
52+
it "creates a record with a ULID primary key" do
53+
event = UlidEvent.create!(
54+
event_type: "user.created",
55+
payload: "{\"user_id\": 1}",
56+
created_at: Time.utc,
57+
updated_at: Time.utc
58+
)
59+
60+
event.id.should_not be_nil
61+
event.id.should be_a(String)
62+
event.id!.size.should eq(26) # ULID is 26 characters
63+
event.event_type.should eq("user.created")
64+
end
65+
66+
it "generates sortable ULIDs" do
67+
event1 = UlidEvent.create!(
68+
event_type: "event1",
69+
payload: "{}",
70+
created_at: Time.utc,
71+
updated_at: Time.utc
72+
)
73+
74+
sleep 1.millisecond # Ensure time difference
75+
76+
event2 = UlidEvent.create!(
77+
event_type: "event2",
78+
payload: "{}",
79+
created_at: Time.utc,
80+
updated_at: Time.utc
81+
)
82+
83+
# ULIDs should be sortable by creation time
84+
(event2.id! > event1.id!).should be_true
85+
end
86+
87+
it "generates unique ULIDs for each record" do
88+
events = 10.times.map do |i|
89+
UlidEvent.create!(
90+
event_type: "event#{i}",
91+
payload: "{}",
92+
created_at: Time.utc,
93+
updated_at: Time.utc
94+
)
95+
end.to_a
96+
97+
ids = events.map(&.id!)
98+
ids.uniq.size.should eq(10)
99+
end
100+
end
101+
102+
context "with hash attributes" do
103+
it "creates a record with a ULID primary key" do
104+
attrs = {
105+
:event_type => "order.placed",
106+
:payload => "{\"order_id\": 123}",
107+
:created_at => Time.utc,
108+
:updated_at => Time.utc,
109+
} of Symbol => DB::Any
110+
111+
event = UlidEvent.create!(attrs)
112+
113+
event.id.should_not be_nil
114+
event.id!.size.should eq(26)
115+
event.event_type.should eq("order.placed")
116+
end
117+
end
118+
119+
context "with model instance" do
120+
it "creates a record and sets the ULID on the instance" do
121+
event = UlidEvent.new("payment.received", "{\"amount\": 99.99}")
122+
event.created_at = Time.utc
123+
event.updated_at = Time.utc
124+
125+
created = UlidEvent.create!(event)
126+
127+
created.id.should_not be_nil
128+
event.id.should eq(created.id)
129+
event.id!.size.should eq(26)
130+
end
131+
end
132+
end
133+
134+
describe "#create!" do
135+
it "creates a record using instance method" do
136+
event = UlidEvent.new("item.shipped", "{\"tracking\": \"ABC123\"}")
137+
event.created_at = Time.utc
138+
event.updated_at = Time.utc
139+
140+
event.create!
141+
142+
event.id.should_not be_nil
143+
event.id!.size.should eq(26)
144+
event.persisted?.should be_true
145+
end
146+
end
147+
148+
describe ".find" do
149+
it "finds a record by ULID" do
150+
event = UlidEvent.create!(
151+
event_type: "findable.event",
152+
payload: "{\"data\": \"test\"}",
153+
created_at: Time.utc,
154+
updated_at: Time.utc
155+
)
156+
157+
found = UlidEvent.find!(event.id!)
158+
159+
found.id.should eq(event.id)
160+
found.event_type.should eq("findable.event")
161+
end
162+
end
163+
164+
describe ".find_or_create_by" do
165+
it "creates a new record if not found" do
166+
event = UlidEvent.find_or_create_by(
167+
event_type: "unique.event",
168+
payload: "{}",
169+
created_at: Time.utc,
170+
updated_at: Time.utc
171+
)
172+
173+
event.id.should_not be_nil
174+
event.id!.size.should eq(26)
175+
end
176+
177+
it "returns existing record if found" do
178+
existing = UlidEvent.create!(
179+
event_type: "existing.event",
180+
payload: "{}",
181+
created_at: Time.utc,
182+
updated_at: Time.utc
183+
)
184+
185+
found = UlidEvent.find_or_create_by(event_type: "existing.event")
186+
187+
found.id.should eq(existing.id)
188+
end
189+
end
190+
191+
describe "ULID format validation" do
192+
it "generates valid ULID format (Crockford Base32)" do
193+
event = UlidEvent.create!(
194+
event_type: "test",
195+
payload: "{}",
196+
created_at: Time.utc,
197+
updated_at: Time.utc
198+
)
199+
200+
# ULID should only contain valid Crockford Base32 characters
201+
# Crockford's Base32: 0-9A-Z excluding I, L, O, U
202+
event.id!.should match(/^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/)
203+
end
204+
205+
it "generates 26-character IDs" do
206+
5.times do |i|
207+
event = UlidEvent.create!(
208+
event_type: "length_test_#{i}",
209+
payload: "{}",
210+
created_at: Time.utc,
211+
updated_at: Time.utc
212+
)
213+
214+
event.id!.size.should eq(26)
215+
end
216+
end
217+
end
218+
219+
describe "multiple record creation" do
220+
it "generates unique ULIDs for 20 records" do
221+
events = 20.times.map do |i|
222+
UlidEvent.create!(
223+
event_type: "bulk_event_#{i}",
224+
payload: "{\"index\": #{i}}",
225+
created_at: Time.utc,
226+
updated_at: Time.utc
227+
)
228+
end.to_a
229+
230+
ids = events.map(&.id!)
231+
ids.uniq.size.should eq(20)
232+
end
233+
end
234+
end

0 commit comments

Comments
 (0)