agent_c 2.9

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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +10 -0
  3. data/.ruby-version +1 -0
  4. data/CLAUDE.md +21 -0
  5. data/README.md +360 -0
  6. data/Rakefile +16 -0
  7. data/TODO.md +105 -0
  8. data/agent_c.gemspec +38 -0
  9. data/docs/batch.md +503 -0
  10. data/docs/chat-methods.md +156 -0
  11. data/docs/cost-reporting.md +86 -0
  12. data/docs/pipeline-tips-and-tricks.md +453 -0
  13. data/docs/session-configuration.md +274 -0
  14. data/docs/testing.md +747 -0
  15. data/docs/tools.md +103 -0
  16. data/docs/versioned-store.md +840 -0
  17. data/lib/agent_c/agent/chat.rb +211 -0
  18. data/lib/agent_c/agent/chat_response.rb +38 -0
  19. data/lib/agent_c/agent/chats/anthropic_bedrock.rb +48 -0
  20. data/lib/agent_c/batch.rb +102 -0
  21. data/lib/agent_c/configs/repo.rb +90 -0
  22. data/lib/agent_c/context.rb +56 -0
  23. data/lib/agent_c/costs/data.rb +39 -0
  24. data/lib/agent_c/costs/report.rb +219 -0
  25. data/lib/agent_c/db/store.rb +162 -0
  26. data/lib/agent_c/errors.rb +19 -0
  27. data/lib/agent_c/pipeline.rb +152 -0
  28. data/lib/agent_c/pipelines/agent.rb +219 -0
  29. data/lib/agent_c/processor.rb +98 -0
  30. data/lib/agent_c/prompts.yml +53 -0
  31. data/lib/agent_c/schema.rb +71 -0
  32. data/lib/agent_c/session.rb +206 -0
  33. data/lib/agent_c/store.rb +72 -0
  34. data/lib/agent_c/test_helpers.rb +173 -0
  35. data/lib/agent_c/tools/dir_glob.rb +46 -0
  36. data/lib/agent_c/tools/edit_file.rb +114 -0
  37. data/lib/agent_c/tools/file_metadata.rb +43 -0
  38. data/lib/agent_c/tools/git_status.rb +30 -0
  39. data/lib/agent_c/tools/grep.rb +119 -0
  40. data/lib/agent_c/tools/paths.rb +36 -0
  41. data/lib/agent_c/tools/read_file.rb +94 -0
  42. data/lib/agent_c/tools/run_rails_test.rb +87 -0
  43. data/lib/agent_c/tools.rb +61 -0
  44. data/lib/agent_c/utils/git.rb +87 -0
  45. data/lib/agent_c/utils/shell.rb +58 -0
  46. data/lib/agent_c/version.rb +5 -0
  47. data/lib/agent_c.rb +32 -0
  48. data/lib/versioned_store/base.rb +314 -0
  49. data/lib/versioned_store/config.rb +26 -0
  50. data/lib/versioned_store/stores/schema.rb +127 -0
  51. data/lib/versioned_store/version.rb +5 -0
  52. data/lib/versioned_store.rb +5 -0
  53. data/template/Gemfile +9 -0
  54. data/template/Gemfile.lock +152 -0
  55. data/template/README.md +61 -0
  56. data/template/Rakefile +50 -0
  57. data/template/bin/rake +27 -0
  58. data/template/lib/autoload.rb +10 -0
  59. data/template/lib/config.rb +59 -0
  60. data/template/lib/pipeline.rb +19 -0
  61. data/template/lib/prompts.yml +57 -0
  62. data/template/lib/store.rb +17 -0
  63. data/template/test/pipeline_test.rb +221 -0
  64. data/template/test/test_helper.rb +18 -0
  65. metadata +194 -0
@@ -0,0 +1,840 @@
1
+ # VersionedStore
2
+
3
+ VersionedStore is a versioned SQLite database abstraction built on ActiveRecord that automatically creates snapshots after each transaction. It provides time-travel capabilities, named snapshots, and a clean DSL for defining records and schemas.
4
+
5
+ ## Key Features
6
+
7
+ - **Automatic Versioning** - Each transaction automatically creates a timestamped snapshot
8
+ - **Time Travel** - Access any previous version of your database
9
+ - **Named Snapshots** - Create and restore from labeled checkpoints
10
+ - **ActiveRecord Integration** - Full ActiveRecord API support (queries, associations, validations)
11
+ - **Schema DSL** - Define tables inline with record definitions
12
+ - **Additive Schema** - Multiple record blocks merge schemas and behaviors
13
+ - **Transaction Safety** - Automatic rollback on errors, no version created on failure
14
+ - **Read-Only Versions** - Historical versions are immutable by default
15
+
16
+ ## Quick Start
17
+
18
+ ```ruby
19
+ # This loads VersionedStore
20
+ # it's a separate gem, but I just inlined it
21
+ # in this project for convenience
22
+ require 'agent_c'
23
+
24
+ # Define your store with records
25
+ class MyStore < VersionedStore::Base
26
+ record(:author) do
27
+ schema do |t|
28
+ t.string(:name)
29
+ t.string(:email)
30
+ end
31
+
32
+ has_many :posts, class_name: class_name(:post)
33
+ end
34
+
35
+ record(:post) do
36
+ schema do |t|
37
+ t.string(:title)
38
+ t.text(:content)
39
+ t.integer(:author_id)
40
+ end
41
+
42
+ belongs_to :author, class_name: class_name(:author)
43
+ end
44
+ end
45
+
46
+ # Initialize the store
47
+ store = MyStore.new( dir: "/tmp/my_data" )
48
+
49
+ # Create records
50
+ author = store.transaction do
51
+ store.author.create!(name: "Jane Doe", email: "jane@example.com")
52
+ end
53
+
54
+ post = store.transaction do
55
+ store.post.create!(
56
+ title: "My First Post",
57
+ content: "Hello world!",
58
+ author: author
59
+ )
60
+ end
61
+
62
+ # Browse versions
63
+ puts "Total versions: #{store.versions.count}"
64
+ store.versions.each_with_index do |version, i|
65
+ puts "Version #{i}: #{version.author.count} authors, #{version.post.count} posts"
66
+ end
67
+ ```
68
+
69
+ ## Defining Records and Schemas
70
+
71
+ ### Inline Schema Definition
72
+
73
+ The most common pattern is defining the schema inline with the record:
74
+
75
+ ```ruby
76
+ class MyStore < VersionedStore::Base
77
+ record(:user) do
78
+ schema do |t|
79
+ t.string(:name)
80
+ t.string(:email)
81
+ t.integer(:age)
82
+ t.boolean(:active, default: true)
83
+ t.timestamps
84
+ end
85
+
86
+ # Add instance methods
87
+ def display_name
88
+ "#{name} <#{email}>"
89
+ end
90
+
91
+ # Add class methods via class_eval
92
+ def self.active_users
93
+ where(active: true)
94
+ end
95
+ end
96
+ end
97
+
98
+ store = MyStore.new( dir: "/tmp/users" )
99
+ user = store.user.create!(name: "Alice", email: "alice@example.com", age: 30)
100
+ puts user.display_name # => "Alice <alice@example.com>"
101
+ ```
102
+
103
+ ### Explicit Table Names
104
+
105
+ By default, the table name is inferred by pluralizing the record name (adds 's'). You can specify a custom table name:
106
+
107
+ ```ruby
108
+ record(:person, table: :people) do
109
+ schema do |t|
110
+ t.string(:name)
111
+ end
112
+ end
113
+ ```
114
+
115
+ ### Additive Record Blocks
116
+
117
+ You can define multiple blocks for the same record. They're additive - schemas merge, methods accumulate:
118
+
119
+ ```ruby
120
+ record(:user) do
121
+ schema do |t|
122
+ t.string(:name)
123
+ end
124
+
125
+ def hello
126
+ "Hello, #{name}!"
127
+ end
128
+ end
129
+
130
+ record(:user) do
131
+ schema do |t|
132
+ t.string(:email)
133
+ end
134
+
135
+ def goodbye
136
+ "Goodbye, #{name}!"
137
+ end
138
+ end
139
+
140
+ # Both columns and both methods are available
141
+ user = store.user.create!(name: "Bob", email: "bob@example.com")
142
+ user.hello # => "Hello, Bob!"
143
+ user.goodbye # => "Goodbye, Bob!"
144
+ ```
145
+
146
+ ### Using Migrations
147
+
148
+ For complex schema changes or when you need more control, use explicit migrations:
149
+
150
+ ```ruby
151
+ class MyStore < VersionedStore::Base
152
+ migrate do
153
+ create_table(:users) do |t|
154
+ t.string(:name)
155
+ t.timestamps
156
+ end
157
+ end
158
+
159
+ migrate do
160
+ add_column(:users, :email, :string)
161
+ add_index(:users, :email, unique: true)
162
+ end
163
+
164
+ record(:user, table: :users) do
165
+ validates :email, uniqueness: true
166
+ end
167
+ end
168
+ ```
169
+
170
+ ## Relationships
171
+
172
+ VersionedStore supports standard ActiveRecord associations. Use `class_name` helper to reference other record classes:
173
+
174
+ ```ruby
175
+ class BlogStore < VersionedStore::Base
176
+ record(:author) do
177
+ schema do |t|
178
+ t.string(:name)
179
+ end
180
+
181
+ has_many(
182
+ :posts,
183
+ foreign_key: :author_id,
184
+ class_name: class_name(:post),
185
+ inverse_of: :author
186
+ )
187
+ end
188
+
189
+ record(:post) do
190
+ schema do |t|
191
+ t.string(:title)
192
+ t.text(:content)
193
+ t.integer(:author_id)
194
+ end
195
+
196
+ belongs_to(
197
+ :author,
198
+ class_name: class_name(:author),
199
+ inverse_of: :posts
200
+ )
201
+
202
+ has_many(
203
+ :comments,
204
+ foreign_key: :post_id,
205
+ class_name: class_name(:comment),
206
+ dependent: :destroy
207
+ )
208
+ end
209
+
210
+ record(:comment) do
211
+ schema do |t|
212
+ t.text(:body)
213
+ t.integer(:post_id)
214
+ end
215
+
216
+ belongs_to(
217
+ :post,
218
+ class_name: class_name(:post)
219
+ )
220
+ end
221
+ end
222
+
223
+ store = BlogStore.new( dir: "/tmp/blog" )
224
+
225
+ author = store.transaction do
226
+ store.author.create!(name: "Alice")
227
+ end
228
+
229
+ post = store.transaction do
230
+ store.post.create!(
231
+ title: "Hello World",
232
+ content: "My first post",
233
+ author: author
234
+ )
235
+ end
236
+
237
+ comment = store.transaction do
238
+ store.comment.create!(
239
+ body: "Great post!",
240
+ post: post
241
+ )
242
+ end
243
+
244
+ # Use relationships
245
+ puts post.author.name # => "Alice"
246
+ puts author.posts.count # => 1
247
+ puts post.comments.first.body # => "Great post!"
248
+ ```
249
+
250
+ ## Transactions and Versioning
251
+
252
+ ### Basic Transactions
253
+
254
+ Every transaction creates a new version snapshot:
255
+
256
+ ```ruby
257
+ store = MyStore.new( dir: "/tmp/data" )
258
+
259
+ store.user.transaction do
260
+ store.user.create!(name: "Alice")
261
+ end
262
+ # Version 1 created
263
+
264
+ store.user.transaction do
265
+ store.user.create!(name: "Bob")
266
+ end
267
+ # Version 2 created
268
+
269
+ puts store.versions.count # => 2
270
+ ```
271
+
272
+ ### Nested Transactions
273
+
274
+ Only the outermost transaction creates a version:
275
+
276
+ ```ruby
277
+ store.transaction do
278
+ user1 = store.user.create!(name: "Alice")
279
+ user2 = store.user.create!(name: "Bob")
280
+
281
+ store.transaction do
282
+ user1.update!(email: "alice@example.com")
283
+ user2.update!(email: "bob@example.com")
284
+ end
285
+ end
286
+ # Only 1 version created for the entire transaction
287
+ ```
288
+
289
+ ### Transaction Rollback
290
+
291
+ If an error occurs, the transaction rolls back and no version is created:
292
+
293
+ ```ruby
294
+ initial_count = store.versions.count
295
+
296
+ begin
297
+ store.transaction do
298
+ store.user.create!(name: "Alice")
299
+ raise "Something went wrong"
300
+ end
301
+ rescue => e
302
+ # Exception caught
303
+ end
304
+
305
+ # No version created - count unchanged
306
+ assert_equal initial_count, store.versions.count
307
+ ```
308
+
309
+ ### Disabling Versioning
310
+
311
+ For high-volume operations or testing, disable versioning:
312
+
313
+ ```ruby
314
+ store = MyStore.new(config: {
315
+ dir: "/tmp/data",
316
+ versioned: false
317
+ })
318
+
319
+ store.user.transaction { store.user.create!(name: "Alice") }
320
+ store.user.transaction { store.user.create!(name: "Bob") }
321
+
322
+ store.versions.count # => 0
323
+ ```
324
+
325
+ ## Accessing Versions
326
+
327
+ ### Listing Versions
328
+
329
+ ```ruby
330
+ store.versions.each_with_index do |version, i|
331
+ puts "Version #{i}:"
332
+ puts " Users: #{version.user.count}"
333
+ puts " Posts: #{version.post.count}"
334
+ end
335
+ ```
336
+
337
+ ### Reading from Versions
338
+
339
+ Version records are read-only:
340
+
341
+ ```ruby
342
+ # Get version 0 (first snapshot)
343
+ v0 = store.versions[0]
344
+
345
+ user = v0.user.first
346
+ puts user.name
347
+
348
+ # Versions are read-only
349
+ user.readonly? # => true
350
+
351
+ # This will raise ActiveRecord::ReadOnlyRecord
352
+ user.update!(name: "New Name")
353
+ ```
354
+
355
+ ### Version Relationships
356
+
357
+ Associations work across versions:
358
+
359
+ ```ruby
360
+ # Create author and post
361
+ author = store.transaction do
362
+ store.author.create!(name: "Alice")
363
+ end
364
+
365
+ post = store.transaction do
366
+ store.post.create!(title: "Hello", author: author)
367
+ end
368
+
369
+ # Access from version
370
+ v1 = store.versions[1]
371
+ v1_post = v1.post.first
372
+ puts v1_post.author.name # => "Alice"
373
+ ```
374
+
375
+ ## Named Snapshots
376
+
377
+ Named snapshots provide labeled checkpoints you can restore to:
378
+
379
+ ```ruby
380
+ store = MyStore.new( dir: "/tmp/data" )
381
+
382
+ # Create initial state
383
+ store.user.transaction { store.user.create!(name: "Alice") }
384
+
385
+ # Create a named snapshot
386
+ store.snapshot("initial_state")
387
+
388
+ # Make more changes
389
+ store.user.transaction { store.user.create!(name: "Bob") }
390
+ store.user.transaction { store.user.create!(name: "Charlie") }
391
+
392
+ # Create another snapshot
393
+ store.snapshot("three_users")
394
+
395
+ # Continue working
396
+ store.user.transaction { store.user.first.update!(name: "Alice Updated") }
397
+
398
+ puts store.user.count # => 3
399
+
400
+ # Restore to "initial_state"
401
+ restored_store = store.restore("initial_state")
402
+
403
+ puts restored_store.user.count # => 1
404
+ puts restored_store.user.first.name # => "Alice"
405
+ ```
406
+
407
+ ## Restoring from Versions
408
+
409
+ ### Version-Based Restore
410
+
411
+ Restore to a specific version by index:
412
+
413
+ ```ruby
414
+ # Create some versions
415
+ store.user.transaction { store.user.create!(name: "Alice") }
416
+ store.user.transaction { store.user.create!(name: "Bob") }
417
+ store.user.transaction { store.user.create!(name: "Charlie") }
418
+
419
+ # Restore to version 1 (after Bob was added)
420
+ restored_store = store.versions[1].restore
421
+
422
+ restored_store.user.count # => 2
423
+ restored_store.user.pluck(:name) # => ["Alice", "Bob"]
424
+
425
+ # Can continue working from restored state
426
+ restored_store.user.transaction do
427
+ restored_store.user.create!(name: "Dave")
428
+ end
429
+ ```
430
+
431
+ ### Named Snapshot Restore
432
+
433
+ ```ruby
434
+ store.snapshot("checkpoint")
435
+
436
+ # Make changes...
437
+
438
+ # Restore by name
439
+ restored_store = store.restore("checkpoint")
440
+ ```
441
+
442
+ ### How Restore Works
443
+
444
+ When you restore:
445
+ 1. The specified version/snapshot is copied to the main database
446
+ 2. Versions newer than the restored version are removed (for version-based restore)
447
+ 3. A new version is created of the restored state
448
+ 4. A new store instance is returned pointing to the restored database
449
+
450
+ ## Configuration
451
+
452
+ ### Directory-Based Configuration
453
+
454
+ Store the database in a directory with default filename:
455
+
456
+ ```ruby
457
+ store = MyStore.new( dir: "/tmp/my_data" )
458
+
459
+ # Creates:
460
+ # /tmp/my_data/db.sqlite3
461
+ # /tmp/my_data/db_versions/
462
+ # /tmp/my_data/db_snapshots/
463
+ ```
464
+
465
+ ### Path-Based Configuration
466
+
467
+ Specify a custom database filename:
468
+
469
+ ```ruby
470
+ store = MyStore.new(
471
+ config: { path: "/tmp/my_data/custom.sqlite3"}
472
+ )
473
+
474
+ # Creates:
475
+ # /tmp/my_data/custom.sqlite3
476
+ # /tmp/my_data/custom_versions/
477
+ # /tmp/my_data/custom_snapshots/
478
+ ```
479
+
480
+ ### Multiple Databases in Same Directory
481
+
482
+ Use path-based configuration for multiple databases:
483
+
484
+ ```ruby
485
+ store1 = MyStore.new( path: "/tmp/data/first.sqlite3" )
486
+ store2 = MyStore.new( path: "/tmp/data/second.sqlite3" )
487
+
488
+ # Creates separate version and snapshot directories:
489
+ # /tmp/data/first.sqlite3
490
+ # /tmp/data/first_versions/
491
+ # /tmp/data/first_snapshots/
492
+ # /tmp/data/second.sqlite3
493
+ # /tmp/data/second_versions/
494
+ # /tmp/data/second_snapshots/
495
+ ```
496
+
497
+ ### Hash Configuration
498
+
499
+ Pass configuration as a hash for convenience:
500
+
501
+ ```ruby
502
+ store = MyStore.new(config: {
503
+ dir: "/tmp/data",
504
+ logger: Logger.new($stdout),
505
+ versioned: true
506
+ })
507
+ ```
508
+
509
+ ### Configuration Options
510
+
511
+ ```ruby
512
+ {
513
+ # Required: specify either dir or path
514
+ dir: "/tmp/data", # Directory for database
515
+ path: "/tmp/data/db.sqlite3", # Or full path to database file
516
+
517
+ # Optional
518
+ logger: Logger.new($stdout), # Logger for debugging
519
+ versioned: true # Enable/disable versioning
520
+ }
521
+ ```
522
+
523
+ ## Accessing Store from Records
524
+
525
+ Both record classes and instances have access to the store:
526
+
527
+ ```ruby
528
+ class MyStore < VersionedStore::Base
529
+ record(:user) do
530
+ schema do |t|
531
+ t.string(:name)
532
+ end
533
+
534
+ def create_post(title)
535
+ # Access store from instance
536
+ store.post.create!(title: title, user: self)
537
+ end
538
+
539
+ def self.with_posts
540
+ # Access store from class
541
+ joins(:posts).distinct
542
+ end
543
+ end
544
+
545
+ record(:post) do
546
+ schema do |t|
547
+ t.string(:title)
548
+ t.integer(:user_id)
549
+ end
550
+
551
+ belongs_to :user, class_name: class_name(:user)
552
+ end
553
+ end
554
+
555
+ store = MyStore.new( dir: "/tmp/data" )
556
+
557
+ user = store.user.create!(name: "Alice")
558
+ post = user.create_post("Hello World")
559
+
560
+ # Class-level store access
561
+ store.user.store == store # => true
562
+
563
+ # Instance-level store access
564
+ user.store == store # => true
565
+ post.store == store # => true
566
+ ```
567
+
568
+ ## Advanced Usage
569
+
570
+ ### Custom Validations
571
+
572
+ ```ruby
573
+ record(:user) do
574
+ schema do |t|
575
+ t.string(:email)
576
+ t.string(:name)
577
+ end
578
+
579
+ validates :email, presence: true, uniqueness: true
580
+ validates :name, length: { minimum: 2 }
581
+
582
+ before_save :normalize_email
583
+
584
+ private
585
+
586
+ def normalize_email
587
+ self.email = email.downcase.strip if email
588
+ end
589
+ end
590
+
591
+ user = store.user.new(name: "A", email: "ALICE@EXAMPLE.COM")
592
+ user.valid? # => false
593
+ user.errors[:name] # => ["is too short (minimum is 2 characters)"]
594
+
595
+ user.name = "Alice"
596
+ user.save! # email normalized to "alice@example.com"
597
+ ```
598
+
599
+ ### Scopes and Queries
600
+
601
+ ```ruby
602
+ record(:post) do
603
+ schema do |t|
604
+ t.string(:title)
605
+ t.boolean(:published, default: false)
606
+ t.timestamps
607
+ end
608
+
609
+ scope :published, -> { where(published: true) }
610
+ scope :recent, -> { order(created_at: :desc).limit(10) }
611
+
612
+ def self.by_title(title)
613
+ where("title LIKE ?", "%#{title}%")
614
+ end
615
+ end
616
+
617
+ # Use scopes
618
+ published_posts = store.post.published
619
+ recent_posts = store.post.recent
620
+ search_results = store.post.by_title("Ruby")
621
+ ```
622
+
623
+ ### JSON and Array Columns
624
+
625
+ ```ruby
626
+ record(:user) do
627
+ schema do |t|
628
+ t.json(:preferences, default: {})
629
+ t.json(:tags, default: [])
630
+ end
631
+ end
632
+
633
+ user = store.user.create!(
634
+ preferences: { theme: "dark", language: "en" },
635
+ tags: ["ruby", "rails"]
636
+ )
637
+
638
+ user.preferences["theme"] # => "dark"
639
+ user.tags # => ["ruby", "rails"]
640
+ ```
641
+
642
+ ## Testing with VersionedStore
643
+
644
+ ### Test Setup
645
+
646
+ ```ruby
647
+ require "minitest/autorun"
648
+ require "agent_c"
649
+
650
+ class MyTest < Minitest::Test
651
+ def setup
652
+ @store_class = Class.new(VersionedStore::Base) do
653
+ record(:user) do
654
+ schema do |t|
655
+ t.string(:name)
656
+ t.string(:email)
657
+ end
658
+ end
659
+ end
660
+
661
+ @store = @store_class.new(dir: Dir.mktmpdir)
662
+ end
663
+
664
+ def test_user_creation
665
+ user = @store.user.create!(name: "Alice", email: "alice@example.com")
666
+
667
+ assert_equal "Alice", user.name
668
+ assert_equal "alice@example.com", user.email
669
+ assert_equal 1, @store.user.count
670
+ end
671
+ end
672
+ ```
673
+
674
+ ### Testing Versioning
675
+
676
+ ```ruby
677
+ def test_versioning
678
+ @store.user.transaction { @store.user.create!(name: "Alice") }
679
+ @store.user.transaction { @store.user.create!(name: "Bob") }
680
+
681
+ assert_equal 2, @store.versions.count
682
+
683
+ # Version 0 has only Alice
684
+ assert_equal 1, @store.versions[0].user.count
685
+ assert_equal "Alice", @store.versions[0].user.first.name
686
+
687
+ # Version 1 has both
688
+ assert_equal 2, @store.versions[1].user.count
689
+ end
690
+ ```
691
+
692
+ ### Testing Restore
693
+
694
+ ```ruby
695
+ def test_restore
696
+ user = @store.user.transaction { @store.user.create!(name: "Alice") }
697
+ @store.user.transaction { user.update!(name: "Alice Updated") }
698
+
699
+ # Restore to version 0
700
+ restored_store = @store.versions[0].restore
701
+
702
+ restored_user = restored_store.user.find(user.id)
703
+ assert_equal "Alice", restored_user.name
704
+ end
705
+ ```
706
+
707
+ ## Common Patterns
708
+
709
+ ### Store as a Mixin
710
+
711
+ ```ruby
712
+ module MyApp
713
+ module Store
714
+ extend ActiveSupport::Concern
715
+
716
+ included do
717
+ record(:workspace) do
718
+ schema do |t|
719
+ t.string(:dir, null: false)
720
+ t.json(:env, default: {})
721
+ end
722
+ end
723
+
724
+ record(:task) do
725
+ schema do |t|
726
+ t.string(:status, default: "pending")
727
+ t.references(:workspace)
728
+ end
729
+
730
+ belongs_to :workspace, class_name: class_name(:workspace)
731
+ end
732
+ end
733
+ end
734
+ end
735
+
736
+ class MyStore < VersionedStore::Base
737
+ include MyApp::Store
738
+
739
+ record(:user) do
740
+ schema do |t|
741
+ t.string(:name)
742
+ end
743
+ end
744
+ end
745
+ ```
746
+
747
+ ### Polymorphic Associations
748
+
749
+ ```ruby
750
+ record(:comment) do
751
+ schema do |t|
752
+ t.text(:body)
753
+ t.string(:commentable_type)
754
+ t.integer(:commentable_id)
755
+ end
756
+
757
+ belongs_to(
758
+ :commentable,
759
+ polymorphic: true
760
+ )
761
+ end
762
+
763
+ record(:post) do
764
+ schema do |t|
765
+ t.string(:title)
766
+ end
767
+
768
+ has_many(
769
+ :comments,
770
+ as: :commentable,
771
+ class_name: class_name(:comment)
772
+ )
773
+ end
774
+
775
+ # Use polymorphic associations
776
+ post = store.post.create!(title: "Hello")
777
+ comment = store.comment.create!(body: "Great post!", commentable: post)
778
+
779
+ comment.commentable == post # => true
780
+ post.comments.first == comment # => true
781
+ ```
782
+
783
+ ## Performance Considerations
784
+
785
+ 1. **Version Size** - Each version is a full database copy. For large databases, consider:
786
+ - Using `versioned: false` for bulk operations
787
+ - Batch multiple operations in a single transaction
788
+ - Periodically clean old versions
789
+
790
+ 2. **Queries** - Standard ActiveRecord performance practices apply:
791
+ - Add indexes for frequently queried columns
792
+ - Use `includes` to avoid N+1 queries
793
+ - Use `find_each` for large result sets
794
+
795
+ 3. **Transactions** - Keep transactions small and focused:
796
+ ```ruby
797
+ # Good: One logical operation
798
+ store.transaction do
799
+ user = store.user.create!(name: "Alice")
800
+ user.posts.create!(title: "Hello")
801
+ end
802
+
803
+ # Avoid: Multiple unrelated operations
804
+ store.transaction do
805
+ store.user.create!(name: "Alice")
806
+ store.category.create!(name: "Tech")
807
+ store.setting.update_all(enabled: true)
808
+ end
809
+ ```
810
+
811
+ ## Reseting the Database
812
+
813
+ You can just delete the directory.
814
+
815
+ ## Troubleshooting
816
+
817
+ ### Version Directory Growing
818
+
819
+ Old versions are never automatically deleted. Manually clean up:
820
+
821
+ ```ruby
822
+ # Remove all but the latest N versions
823
+ def cleanup_versions(store, keep_count = 10)
824
+ version_files = Dir.glob(File.join(store.send(:versions_dir), "*.sqlite3")).sort
825
+ to_delete = version_files[0..-(keep_count + 1)]
826
+ to_delete.each { |f| File.delete(f) }
827
+ end
828
+ ```
829
+
830
+ ### Migration Not Running
831
+
832
+ Migrations only run on root store initialization:
833
+
834
+ ```ruby
835
+ # This runs migrations
836
+ store = MyStore.new( dir: "/tmp/data" )
837
+
838
+ # This doesn't run migrations (accessing existing snapshot)
839
+ version_store = store.versions[0]
840
+ ```