htm 0.0.14 → 0.0.15

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/README.md +269 -79
  4. data/db/migrate/00003_create_file_sources.rb +5 -0
  5. data/db/migrate/00004_create_nodes.rb +17 -0
  6. data/db/migrate/00005_create_tags.rb +7 -0
  7. data/db/migrate/00006_create_node_tags.rb +2 -0
  8. data/db/migrate/00007_create_robot_nodes.rb +7 -0
  9. data/db/schema.sql +41 -29
  10. data/docs/api/yard/HTM/Configuration.md +54 -0
  11. data/docs/api/yard/HTM/Database.md +13 -10
  12. data/docs/api/yard/HTM/EmbeddingService.md +5 -1
  13. data/docs/api/yard/HTM/LongTermMemory.md +18 -277
  14. data/docs/api/yard/HTM/PropositionError.md +18 -0
  15. data/docs/api/yard/HTM/PropositionService.md +66 -0
  16. data/docs/api/yard/HTM/QueryCache.md +88 -0
  17. data/docs/api/yard/HTM/RobotGroup.md +481 -0
  18. data/docs/api/yard/HTM/SqlBuilder.md +108 -0
  19. data/docs/api/yard/HTM/TagService.md +4 -0
  20. data/docs/api/yard/HTM/Telemetry/NullInstrument.md +13 -0
  21. data/docs/api/yard/HTM/Telemetry/NullMeter.md +15 -0
  22. data/docs/api/yard/HTM/Telemetry.md +109 -0
  23. data/docs/api/yard/HTM/WorkingMemoryChannel.md +176 -0
  24. data/docs/api/yard/HTM.md +11 -23
  25. data/docs/api/yard/index.csv +102 -25
  26. data/docs/api/yard-reference.md +8 -0
  27. data/docs/assets/images/multi-provider-failover.svg +51 -0
  28. data/docs/assets/images/robot-group-architecture.svg +65 -0
  29. data/docs/database/README.md +3 -3
  30. data/docs/database/public.file_sources.svg +29 -21
  31. data/docs/database/public.node_tags.md +2 -0
  32. data/docs/database/public.node_tags.svg +53 -41
  33. data/docs/database/public.nodes.md +2 -0
  34. data/docs/database/public.nodes.svg +52 -40
  35. data/docs/database/public.robot_nodes.md +2 -0
  36. data/docs/database/public.robot_nodes.svg +30 -22
  37. data/docs/database/public.robots.svg +16 -12
  38. data/docs/database/public.tags.md +3 -0
  39. data/docs/database/public.tags.svg +41 -33
  40. data/docs/database/schema.json +66 -0
  41. data/docs/database/schema.svg +60 -48
  42. data/docs/development/index.md +13 -0
  43. data/docs/development/rake-tasks.md +1068 -0
  44. data/docs/getting-started/quick-start.md +144 -155
  45. data/docs/guides/adding-memories.md +2 -3
  46. data/docs/guides/context-assembly.md +185 -184
  47. data/docs/guides/getting-started.md +154 -148
  48. data/docs/guides/index.md +7 -0
  49. data/docs/guides/long-term-memory.md +60 -92
  50. data/docs/guides/mcp-server.md +617 -0
  51. data/docs/guides/multi-robot.md +249 -345
  52. data/docs/guides/recalling-memories.md +153 -163
  53. data/docs/guides/robot-groups.md +604 -0
  54. data/docs/guides/search-strategies.md +61 -58
  55. data/docs/guides/working-memory.md +103 -136
  56. data/docs/index.md +30 -26
  57. data/examples/robot_groups/robot_worker.rb +1 -2
  58. data/examples/robot_groups/same_process.rb +1 -4
  59. data/lib/htm/robot_group.rb +721 -0
  60. data/lib/htm/version.rb +1 -1
  61. data/lib/htm/working_memory_channel.rb +250 -0
  62. data/lib/htm.rb +2 -0
  63. data/mkdocs.yml +2 -0
  64. metadata +18 -9
  65. data/db/migrate/00009_add_working_memory_to_robot_nodes.rb +0 -12
  66. data/db/migrate/00010_add_soft_delete_to_associations.rb +0 -29
  67. data/db/migrate/00011_add_performance_indexes.rb +0 -21
  68. data/db/migrate/00012_add_tags_trigram_index.rb +0 -18
  69. data/db/migrate/00013_enable_lz4_compression.rb +0 -43
  70. data/examples/robot_groups/lib/robot_group.rb +0 -419
  71. data/examples/robot_groups/lib/working_memory_channel.rb +0 -140
data/lib/htm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class HTM
4
- VERSION = '0.0.14'
4
+ VERSION = '0.0.15'
5
5
  end
@@ -0,0 +1,250 @@
1
+ # examples/robot_groups/lib/htm/working_memory_channel.rb
2
+ # frozen_string_literal: true
3
+
4
+ class HTM
5
+ # Provides real-time synchronization of working memory changes across multiple
6
+ # robots using PostgreSQL LISTEN/NOTIFY pub/sub mechanism.
7
+ #
8
+ # This class enables distributed robots to maintain synchronized working memory
9
+ # by broadcasting change notifications through PostgreSQL channels. When one robot
10
+ # adds, evicts, or clears working memory, all other robots in the group receive
11
+ # immediate notification.
12
+ #
13
+ # @example Basic usage
14
+ # channel = HTM::WorkingMemoryChannel.new('support-team', db_config)
15
+ #
16
+ # # Subscribe to changes
17
+ # channel.on_change do |event, node_id, robot_id|
18
+ # case event
19
+ # when :added then puts "Node #{node_id} added by robot #{robot_id}"
20
+ # when :evicted then puts "Node #{node_id} evicted by robot #{robot_id}"
21
+ # when :cleared then puts "Working memory cleared by robot #{robot_id}"
22
+ # end
23
+ # end
24
+ #
25
+ # # Start listening in background thread
26
+ # channel.start_listening
27
+ #
28
+ # # Publish a change
29
+ # channel.notify(:added, node_id: 123, robot_id: 456)
30
+ #
31
+ # # Cleanup when done
32
+ # channel.stop_listening
33
+ #
34
+ # @see HTM::RobotGroup Higher-level coordination using this channel
35
+ #
36
+ class WorkingMemoryChannel
37
+ # Prefix used for all PostgreSQL channel names
38
+ # @return [String]
39
+ CHANNEL_PREFIX = 'htm_wm'
40
+
41
+ # Number of notifications received since channel was created
42
+ # @return [Integer]
43
+ attr_reader :notifications_received
44
+
45
+ # Creates a new working memory channel for a robot group.
46
+ #
47
+ # The channel name is derived from the group name with non-alphanumeric
48
+ # characters replaced by underscores to ensure PostgreSQL compatibility.
49
+ #
50
+ # @param group_name [String] Name of the robot group (used to create unique channel)
51
+ # @param db_config [Hash] PostgreSQL connection configuration hash
52
+ # @option db_config [String] :host Database host
53
+ # @option db_config [Integer] :port Database port
54
+ # @option db_config [String] :dbname Database name
55
+ # @option db_config [String] :user Database user
56
+ # @option db_config [String] :password Database password (optional)
57
+ #
58
+ # @example
59
+ # db_config = { host: 'localhost', port: 5432, dbname: 'htm_dev', user: 'postgres' }
60
+ # channel = HTM::WorkingMemoryChannel.new('customer-support', db_config)
61
+ #
62
+ def initialize(group_name, db_config)
63
+ @group_name = group_name
64
+ @channel = "#{CHANNEL_PREFIX}_#{group_name.gsub(/[^a-z0-9_]/i, '_')}"
65
+ @db_config = db_config
66
+ @listeners = []
67
+ @listen_thread = nil
68
+ @stop_requested = false
69
+ @notifications_received = 0
70
+ @mutex = Mutex.new
71
+ end
72
+
73
+ # @!group Publishing
74
+
75
+ # Broadcasts a working memory change notification to all listeners.
76
+ #
77
+ # Uses PostgreSQL's pg_notify function to send a JSON payload containing
78
+ # the event type, affected node ID, originating robot ID, and timestamp.
79
+ #
80
+ # @param event [Symbol] Type of change (:added, :evicted, or :cleared)
81
+ # @param node_id [Integer, nil] ID of the affected node (nil for :cleared events)
82
+ # @param robot_id [Integer] ID of the robot that triggered the change
83
+ # @return [void]
84
+ #
85
+ # @example Notify that a node was added
86
+ # channel.notify(:added, node_id: 123, robot_id: 1)
87
+ #
88
+ # @example Notify that working memory was cleared
89
+ # channel.notify(:cleared, node_id: nil, robot_id: 1)
90
+ #
91
+ def notify(event, node_id:, robot_id:)
92
+ payload = {
93
+ event: event,
94
+ node_id: node_id,
95
+ robot_id: robot_id,
96
+ timestamp: Time.now.iso8601
97
+ }.to_json
98
+
99
+ with_connection do |conn|
100
+ conn.exec_params('SELECT pg_notify($1, $2)', [@channel, payload])
101
+ end
102
+ end
103
+
104
+ # @!endgroup
105
+
106
+ # @!group Subscribing
107
+
108
+ # Registers a callback to be invoked when working memory changes occur.
109
+ #
110
+ # Multiple callbacks can be registered; all will be called for each event.
111
+ # Callbacks are invoked synchronously within the listener thread.
112
+ #
113
+ # @yield [event, node_id, robot_id] Block called for each notification
114
+ # @yieldparam event [Symbol] Type of change (:added, :evicted, or :cleared)
115
+ # @yieldparam node_id [Integer, nil] ID of the affected node
116
+ # @yieldparam robot_id [Integer] ID of the robot that triggered the change
117
+ # @return [void]
118
+ #
119
+ # @example Register a change handler
120
+ # channel.on_change do |event, node_id, robot_id|
121
+ # puts "Received #{event} event for node #{node_id}"
122
+ # end
123
+ #
124
+ def on_change(&callback)
125
+ @mutex.synchronize { @listeners << callback }
126
+ end
127
+
128
+ # Starts listening for notifications in a background thread.
129
+ #
130
+ # Creates a dedicated PostgreSQL connection that uses LISTEN to receive
131
+ # notifications. The thread polls every 0.5 seconds, allowing for clean
132
+ # shutdown via {#stop_listening}.
133
+ #
134
+ # @return [Thread] The background listener thread
135
+ #
136
+ # @example Start and verify listening
137
+ # thread = channel.start_listening
138
+ # puts "Listening: #{channel.listening?}" # => true
139
+ #
140
+ def start_listening
141
+ @stop_requested = false
142
+ @listen_thread = Thread.new do
143
+ listen_loop
144
+ end
145
+ @listen_thread.abort_on_exception = true
146
+ @listen_thread
147
+ end
148
+
149
+ # Stops the background listener thread.
150
+ #
151
+ # Signals the listener to stop, waits up to 0.5 seconds for clean exit,
152
+ # then forcefully terminates if still running. The PostgreSQL connection
153
+ # is closed automatically.
154
+ #
155
+ # @return [void]
156
+ #
157
+ # @example Clean shutdown
158
+ # channel.stop_listening
159
+ # puts "Listening: #{channel.listening?}" # => false
160
+ #
161
+ def stop_listening
162
+ @stop_requested = true
163
+ # Give the thread a moment to exit cleanly
164
+ @listen_thread&.join(0.5)
165
+ @listen_thread&.kill if @listen_thread&.alive?
166
+ @listen_thread = nil
167
+ end
168
+
169
+ # @!endgroup
170
+
171
+ # @!group Status
172
+
173
+ # Checks if the listener thread is currently active.
174
+ #
175
+ # @return [Boolean] true if listening for notifications, false otherwise
176
+ #
177
+ # @example
178
+ # channel.start_listening
179
+ # channel.listening? # => true
180
+ # channel.stop_listening
181
+ # channel.listening? # => false
182
+ #
183
+ def listening?
184
+ @listen_thread&.alive? || false
185
+ end
186
+
187
+ # Returns the PostgreSQL channel name used for notifications.
188
+ #
189
+ # The channel name is derived from the group name with a prefix and
190
+ # sanitization of special characters.
191
+ #
192
+ # @return [String] The PostgreSQL LISTEN/NOTIFY channel name
193
+ #
194
+ # @example
195
+ # channel = HTM::WorkingMemoryChannel.new('my-group', db_config)
196
+ # channel.channel_name # => "htm_wm_my_group"
197
+ #
198
+ def channel_name
199
+ @channel
200
+ end
201
+
202
+ # @!endgroup
203
+
204
+ private
205
+
206
+ def listen_loop
207
+ conn = PG.connect(@db_config)
208
+ conn.exec("LISTEN #{conn.escape_identifier(@channel)}")
209
+
210
+ until @stop_requested
211
+ # Wait for notification with timeout (allows checking @stop_requested)
212
+ conn.wait_for_notify(0.5) do |_channel, _pid, payload|
213
+ handle_notification(payload)
214
+ end
215
+ end
216
+ rescue PG::Error => e
217
+ unless @stop_requested
218
+ HTM.logger.error "WorkingMemoryChannel error: #{e.message}"
219
+ sleep 1
220
+ retry
221
+ end
222
+ ensure
223
+ conn&.close
224
+ end
225
+
226
+ def handle_notification(payload)
227
+ data = JSON.parse(payload, symbolize_names: true)
228
+
229
+ @mutex.synchronize do
230
+ @notifications_received += 1
231
+ @listeners.each do |callback|
232
+ callback.call(
233
+ data[:event].to_sym,
234
+ data[:node_id],
235
+ data[:robot_id]
236
+ )
237
+ end
238
+ end
239
+ rescue JSON::ParserError => e
240
+ HTM.logger.error "Invalid notification payload: #{e.message}"
241
+ end
242
+
243
+ def with_connection
244
+ conn = PG.connect(@db_config)
245
+ yield conn
246
+ ensure
247
+ conn&.close
248
+ end
249
+ end
250
+ end
data/lib/htm.rb CHANGED
@@ -21,6 +21,8 @@ require_relative "htm/loaders/markdown_chunker"
21
21
  require_relative "htm/loaders/markdown_loader"
22
22
  require_relative "htm/observability"
23
23
  require_relative "htm/telemetry"
24
+ require_relative "htm/working_memory_channel"
25
+ require_relative "htm/robot_group"
24
26
 
25
27
  require "pg"
26
28
  require "securerandom"
data/mkdocs.yml CHANGED
@@ -199,6 +199,7 @@ nav:
199
199
  - Multi-Robot Usage: guides/multi-robot.md
200
200
  - Search Strategies: guides/search-strategies.md
201
201
  - Context Assembly: guides/context-assembly.md
202
+ - MCP Server: guides/mcp-server.md
202
203
  - API Reference:
203
204
  - api/index.md
204
205
  - HTM Class: api/htm.md
@@ -245,6 +246,7 @@ nav:
245
246
  - Setup: development/setup.md
246
247
  - Testing: development/testing.md
247
248
  - Contributing: development/contributing.md
249
+ - Rake Tasks Reference: development/rake-tasks.md
248
250
  - Database Schema: development/schema.md
249
251
  - Database Tables:
250
252
  - database/README.md
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: htm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.14
4
+ version: 0.0.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -299,11 +299,6 @@ files:
299
299
  - db/migrate/00005_create_tags.rb
300
300
  - db/migrate/00006_create_node_tags.rb
301
301
  - db/migrate/00007_create_robot_nodes.rb
302
- - db/migrate/00009_add_working_memory_to_robot_nodes.rb
303
- - db/migrate/00010_add_soft_delete_to_associations.rb
304
- - db/migrate/00011_add_performance_indexes.rb
305
- - db/migrate/00012_add_tags_trigram_index.rb
306
- - db/migrate/00013_enable_lz4_compression.rb
307
302
  - db/schema.sql
308
303
  - db/seed_data/README.md
309
304
  - db/seed_data/presidents.md
@@ -331,17 +326,26 @@ files:
331
326
  - docs/api/yard/HTM/LongTermMemory.md
332
327
  - docs/api/yard/HTM/NotFoundError.md
333
328
  - docs/api/yard/HTM/Observability.md
329
+ - docs/api/yard/HTM/PropositionError.md
330
+ - docs/api/yard/HTM/PropositionService.md
331
+ - docs/api/yard/HTM/QueryCache.md
334
332
  - docs/api/yard/HTM/QueryTimeoutError.md
335
333
  - docs/api/yard/HTM/Railtie.md
336
334
  - docs/api/yard/HTM/ResourceExhaustedError.md
335
+ - docs/api/yard/HTM/RobotGroup.md
336
+ - docs/api/yard/HTM/SqlBuilder.md
337
337
  - docs/api/yard/HTM/TagError.md
338
338
  - docs/api/yard/HTM/TagService.md
339
+ - docs/api/yard/HTM/Telemetry.md
340
+ - docs/api/yard/HTM/Telemetry/NullInstrument.md
341
+ - docs/api/yard/HTM/Telemetry/NullMeter.md
339
342
  - docs/api/yard/HTM/Timeframe.md
340
343
  - docs/api/yard/HTM/Timeframe/Result.md
341
344
  - docs/api/yard/HTM/TimeframeExtractor.md
342
345
  - docs/api/yard/HTM/TimeframeExtractor/Result.md
343
346
  - docs/api/yard/HTM/ValidationError.md
344
347
  - docs/api/yard/HTM/WorkingMemory.md
348
+ - docs/api/yard/HTM/WorkingMemoryChannel.md
345
349
  - docs/api/yard/index.csv
346
350
  - docs/architecture/adrs/001-postgresql-timescaledb.md
347
351
  - docs/architecture/adrs/002-two-tier-memory.md
@@ -381,7 +385,9 @@ files:
381
385
  - docs/assets/images/htm-working-memory-architecture.svg
382
386
  - docs/assets/images/htm.jpg
383
387
  - docs/assets/images/htm_demo.gif
388
+ - docs/assets/images/multi-provider-failover.svg
384
389
  - docs/assets/images/project-structure.svg
390
+ - docs/assets/images/robot-group-architecture.svg
385
391
  - docs/assets/images/test-directory-structure.svg
386
392
  - docs/assets/js/mathjax.js
387
393
  - docs/assets/videos/htm_video.mp4
@@ -419,6 +425,7 @@ files:
419
425
  - docs/database_rake_tasks.md
420
426
  - docs/development/contributing.md
421
427
  - docs/development/index.md
428
+ - docs/development/rake-tasks.md
422
429
  - docs/development/schema.md
423
430
  - docs/development/setup.md
424
431
  - docs/development/testing.md
@@ -430,8 +437,10 @@ files:
430
437
  - docs/guides/getting-started.md
431
438
  - docs/guides/index.md
432
439
  - docs/guides/long-term-memory.md
440
+ - docs/guides/mcp-server.md
433
441
  - docs/guides/multi-robot.md
434
442
  - docs/guides/recalling-memories.md
443
+ - docs/guides/robot-groups.md
435
444
  - docs/guides/search-strategies.md
436
445
  - docs/guides/working-memory.md
437
446
  - docs/images/htm-er-diagram.svg
@@ -450,8 +459,6 @@ files:
450
459
  - examples/example_app/app.rb
451
460
  - examples/file_loader_usage.rb
452
461
  - examples/mcp_client.rb
453
- - examples/robot_groups/lib/robot_group.rb
454
- - examples/robot_groups/lib/working_memory_channel.rb
455
462
  - examples/robot_groups/multi_process.rb
456
463
  - examples/robot_groups/robot_worker.rb
457
464
  - examples/robot_groups/same_process.rb
@@ -495,6 +502,7 @@ files:
495
502
  - lib/htm/proposition_service.rb
496
503
  - lib/htm/query_cache.rb
497
504
  - lib/htm/railtie.rb
505
+ - lib/htm/robot_group.rb
498
506
  - lib/htm/sql_builder.rb
499
507
  - lib/htm/tag_service.rb
500
508
  - lib/htm/tasks.rb
@@ -503,6 +511,7 @@ files:
503
511
  - lib/htm/timeframe_extractor.rb
504
512
  - lib/htm/version.rb
505
513
  - lib/htm/working_memory.rb
514
+ - lib/htm/working_memory_channel.rb
506
515
  - lib/tasks/db.rake
507
516
  - lib/tasks/doc.rake
508
517
  - lib/tasks/files.rake
@@ -532,7 +541,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
532
541
  - !ruby/object:Gem::Version
533
542
  version: '0'
534
543
  requirements: []
535
- rubygems_version: 4.0.0.dev
544
+ rubygems_version: 4.0.0
536
545
  specification_version: 4
537
546
  summary: Hierarchical Temporal Memory for LLM robots
538
547
  test_files: []
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class AddWorkingMemoryToRobotNodes < ActiveRecord::Migration[7.1]
4
- def change
5
- add_column :robot_nodes, :working_memory, :boolean, default: false, null: false,
6
- comment: 'True if this node is currently in the robot working memory'
7
-
8
- add_index :robot_nodes, [:robot_id, :working_memory],
9
- where: 'working_memory = true',
10
- name: 'idx_robot_nodes_working_memory'
11
- end
12
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class AddSoftDeleteToAssociations < ActiveRecord::Migration[7.0]
4
- def change
5
- # Add deleted_at to robot_nodes for soft delete support
6
- unless column_exists?(:robot_nodes, :deleted_at)
7
- add_column :robot_nodes, :deleted_at, :datetime, null: true
8
- end
9
- unless index_exists?(:robot_nodes, :deleted_at)
10
- add_index :robot_nodes, :deleted_at
11
- end
12
-
13
- # Add deleted_at to node_tags for soft delete support
14
- unless column_exists?(:node_tags, :deleted_at)
15
- add_column :node_tags, :deleted_at, :datetime, null: true
16
- end
17
- unless index_exists?(:node_tags, :deleted_at)
18
- add_index :node_tags, :deleted_at
19
- end
20
-
21
- # Add deleted_at to tags for soft delete support
22
- unless column_exists?(:tags, :deleted_at)
23
- add_column :tags, :deleted_at, :datetime, null: true
24
- end
25
- unless index_exists?(:tags, :deleted_at)
26
- add_index :tags, :deleted_at
27
- end
28
- end
29
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class AddPerformanceIndexes < ActiveRecord::Migration[7.1]
4
- def change
5
- # Partial index for soft-delete filter (used in almost every query)
6
- # This complements idx_nodes_not_deleted_created_at for queries that
7
- # don't need created_at ordering but still filter by deleted_at IS NULL
8
- add_index :nodes, :id,
9
- name: 'idx_nodes_active',
10
- where: 'deleted_at IS NULL',
11
- comment: 'Partial index for active (non-deleted) node queries'
12
-
13
- # Composite index for embedding-based searches on active nodes
14
- # Helps vector_search and hybrid_search which filter by deleted_at IS NULL
15
- # and embedding IS NOT NULL
16
- execute <<-SQL
17
- CREATE INDEX idx_nodes_active_with_embedding ON nodes (id)
18
- WHERE deleted_at IS NULL AND embedding IS NOT NULL
19
- SQL
20
- end
21
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class AddTagsTrigramIndex < ActiveRecord::Migration[7.0]
4
- def up
5
- # Add GIN trigram index on tags.name for fuzzy search
6
- # Enables queries like: WHERE name % 'postgrsql' (typo-tolerant)
7
- # Also speeds up LIKE '%pattern%' queries
8
- execute <<~SQL
9
- CREATE INDEX idx_tags_name_trgm ON tags USING gin(name gin_trgm_ops);
10
- SQL
11
- end
12
-
13
- def down
14
- execute <<~SQL
15
- DROP INDEX IF EXISTS idx_tags_name_trgm;
16
- SQL
17
- end
18
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class EnableLz4Compression < ActiveRecord::Migration[7.0]
4
- def up
5
- # Switch TOAST compression from pglz to lz4 for better read performance
6
- # LZ4 decompression is ~32% faster than pglz with only marginally lower compression ratio
7
- # See: https://www.depesz.com/2025/11/29/using-json-json-vs-jsonb-pglz-vs-lz4-key-optimization-parsing-speed/
8
-
9
- # nodes.metadata - JSONB column for flexible key-value storage
10
- execute <<~SQL
11
- ALTER TABLE nodes ALTER COLUMN metadata SET COMPRESSION lz4;
12
- SQL
13
-
14
- # nodes.content - TEXT column containing memory content
15
- execute <<~SQL
16
- ALTER TABLE nodes ALTER COLUMN content SET COMPRESSION lz4;
17
- SQL
18
-
19
- # file_sources.frontmatter - JSONB column for parsed YAML frontmatter
20
- execute <<~SQL
21
- ALTER TABLE file_sources ALTER COLUMN frontmatter SET COMPRESSION lz4;
22
- SQL
23
-
24
- # Note: Existing rows retain their original compression until rewritten.
25
- # To recompress existing data, run: VACUUM FULL nodes; VACUUM FULL file_sources;
26
- # This is optional and can be done during a maintenance window.
27
- end
28
-
29
- def down
30
- # Revert to default pglz compression
31
- execute <<~SQL
32
- ALTER TABLE nodes ALTER COLUMN metadata SET COMPRESSION pglz;
33
- SQL
34
-
35
- execute <<~SQL
36
- ALTER TABLE nodes ALTER COLUMN content SET COMPRESSION pglz;
37
- SQL
38
-
39
- execute <<~SQL
40
- ALTER TABLE file_sources ALTER COLUMN frontmatter SET COMPRESSION pglz;
41
- SQL
42
- end
43
- end