pg_eventstore 2.0.0 → 3.0.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.
Files changed (303) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +10 -1
  3. data/CHANGELOG.md +186 -20
  4. data/README.md +25 -15
  5. data/db/migrations/10_setup_pg_cron.rb +1 -23
  6. data/db/migrations/15_create_streams_global_index.sql +17 -0
  7. data/db/migrations/16_populate_streams_global_index.rb +150 -0
  8. data/db/migrations/17_create_events_global_index.sql +62 -0
  9. data/db/migrations/18_populate_events_global_index.rb +124 -0
  10. data/db/migrations/19_populate_events_global_index_p2.rb +25 -0
  11. data/db/migrations/20_migrate_subscriptions_current_position.sql +19 -0
  12. data/db/migrations/21_create_partitions_stats.sql +2 -0
  13. data/db/migrations/22_drop_streams_sql_view.sql +2 -0
  14. data/db/migrations/23_adjust_partitions_indexes.sql +5 -0
  15. data/db/migrations/24_drop_legacy_events_indexes.sql +3 -0
  16. data/db/migrations/25_add_parent_partitions_info_to_partitions.sql +16 -0
  17. data/db/migrations/26_change_events_stream_revision_type.rb +35 -0
  18. data/db/migrations/27_drop_events_horizon.sql +3 -0
  19. data/db/migrations/28_create_maintenance_tasks.sql +8 -0
  20. data/db/migrations/29_create_event_markers.sql +35 -0
  21. data/db/migrations/30_improve_partitions_search_in_admin_ui.rb +24 -0
  22. data/db/migrations/31_drop_events_id_default_value.sql +1 -0
  23. data/db/migrations/32_create_parittions_triggers.sql +101 -0
  24. data/docs/AGENTS.fragment.md +696 -0
  25. data/docs/admin_ui.md +3 -1
  26. data/docs/appending_events.md +110 -49
  27. data/docs/configuration.md +69 -36
  28. data/docs/event_tracing.md +55 -0
  29. data/docs/events_and_streams.md +54 -30
  30. data/docs/how_it_works.md +57 -16
  31. data/docs/linking_events.md +2 -1
  32. data/docs/maintenance.md +14 -5
  33. data/docs/multiple_commands.md +17 -8
  34. data/docs/reading_events.md +208 -43
  35. data/docs/reading_streams.md +50 -0
  36. data/docs/replication.md +99 -0
  37. data/docs/subscriptions.md +165 -35
  38. data/docs/writing_middleware.md +25 -8
  39. data/ext/pg_eventstore_ext/extconf.rb +5 -0
  40. data/ext/pg_eventstore_ext/pg_eventstore_ext.c +74 -0
  41. data/lib/pg_eventstore/async_runner.rb +63 -0
  42. data/lib/pg_eventstore/basic_config.rb +15 -0
  43. data/lib/pg_eventstore/chunks/chunk.rb +31 -0
  44. data/lib/pg_eventstore/chunks/read_api_events_index_chunk.rb +83 -0
  45. data/lib/pg_eventstore/chunks/replica_events_index_chunk.rb +35 -0
  46. data/lib/pg_eventstore/chunks/repository.rb +73 -0
  47. data/lib/pg_eventstore/chunks/subscription_checkpoint_chunk.rb +48 -0
  48. data/lib/pg_eventstore/chunks/subscription_events_index_chunk.rb +99 -0
  49. data/lib/pg_eventstore/chunks.rb +14 -0
  50. data/lib/pg_eventstore/client.rb +163 -50
  51. data/lib/pg_eventstore/commands/append.rb +62 -43
  52. data/lib/pg_eventstore/commands/delete_event.rb +7 -8
  53. data/lib/pg_eventstore/commands/delete_stream.rb +3 -3
  54. data/lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb +9 -7
  55. data/lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb +12 -7
  56. data/lib/pg_eventstore/commands/link_to.rb +5 -5
  57. data/lib/pg_eventstore/commands/read.rb +23 -5
  58. data/lib/pg_eventstore/commands/read_grouped.rb +46 -0
  59. data/lib/pg_eventstore/commands/read_streams.rb +18 -0
  60. data/lib/pg_eventstore/commands/read_streams_paginated.rb +56 -0
  61. data/lib/pg_eventstore/commands/regular_stream_read_paginated.rb +4 -4
  62. data/lib/pg_eventstore/commands/revision_check/current_revision.rb +51 -0
  63. data/lib/pg_eventstore/commands/revision_check/event_type_revisions_comparison.rb +46 -0
  64. data/lib/pg_eventstore/commands/revision_check/expected_revision.rb +135 -0
  65. data/lib/pg_eventstore/commands/revision_check/stream_revision_check.rb +98 -0
  66. data/lib/pg_eventstore/commands/revision_check/stream_revision_comparison.rb +36 -0
  67. data/lib/pg_eventstore/commands/stream_revision.rb +14 -0
  68. data/lib/pg_eventstore/commands/system_stream_read_paginated.rb +4 -4
  69. data/lib/pg_eventstore/commands.rb +4 -2
  70. data/lib/pg_eventstore/config.rb +30 -9
  71. data/lib/pg_eventstore/connection.rb +21 -2
  72. data/lib/pg_eventstore/errors.rb +136 -10
  73. data/lib/pg_eventstore/event.rb +34 -2
  74. data/lib/pg_eventstore/event_deserializer.rb +5 -4
  75. data/lib/pg_eventstore/event_global_index.rb +118 -0
  76. data/lib/pg_eventstore/event_marker.rb +16 -0
  77. data/lib/pg_eventstore/event_marker_index.rb +25 -0
  78. data/lib/pg_eventstore/event_serializer.rb +6 -0
  79. data/lib/pg_eventstore/extensions/acts_as_configurable.rb +88 -0
  80. data/lib/pg_eventstore/extensions/options_defaults.rb +25 -0
  81. data/lib/pg_eventstore/extensions/options_extension.rb +2 -2
  82. data/lib/pg_eventstore/feature_marker.rb +18 -0
  83. data/lib/pg_eventstore/maintenance.rb +11 -6
  84. data/lib/pg_eventstore/middleware/event_tracing.rb +53 -0
  85. data/lib/pg_eventstore/partition.rb +6 -0
  86. data/lib/pg_eventstore/pg_connection.rb +70 -14
  87. data/lib/pg_eventstore/queries/event_marker_queries.rb +74 -0
  88. data/lib/pg_eventstore/queries/event_queries.rb +7 -93
  89. data/lib/pg_eventstore/queries/events_global_index_queries.rb +131 -0
  90. data/lib/pg_eventstore/queries/index_filtering_queries.rb +179 -0
  91. data/lib/pg_eventstore/queries/links_resolver.rb +4 -4
  92. data/lib/pg_eventstore/queries/maintenance_queries.rb +159 -42
  93. data/lib/pg_eventstore/queries/partition_queries.rb +45 -103
  94. data/lib/pg_eventstore/queries/replica_queries.rb +110 -0
  95. data/lib/pg_eventstore/queries/streams_global_index_queries.rb +163 -0
  96. data/lib/pg_eventstore/queries/transaction_queries.rb +52 -10
  97. data/lib/pg_eventstore/queries.rb +29 -1
  98. data/lib/pg_eventstore/query_builders/basic_filtering.rb +38 -5
  99. data/lib/pg_eventstore/query_builders/event_markers_filtering.rb +47 -0
  100. data/lib/pg_eventstore/query_builders/event_markers_index_filtering.rb +275 -0
  101. data/lib/pg_eventstore/query_builders/event_subscription_positions_filtering.rb +37 -0
  102. data/lib/pg_eventstore/query_builders/events_filtering.rb +15 -164
  103. data/lib/pg_eventstore/query_builders/events_global_index_filtering.rb +227 -0
  104. data/lib/pg_eventstore/query_builders/filters/collection.rb +324 -0
  105. data/lib/pg_eventstore/query_builders/filters/event_type_filter.rb +37 -0
  106. data/lib/pg_eventstore/query_builders/filters/filter_row.rb +54 -0
  107. data/lib/pg_eventstore/query_builders/filters/marker_filter.rb +20 -0
  108. data/lib/pg_eventstore/query_builders/filters/marker_filter_row.rb +49 -0
  109. data/lib/pg_eventstore/query_builders/filters/stream_filter.rb +48 -0
  110. data/lib/pg_eventstore/query_builders/index_based_events_filtering.rb +260 -0
  111. data/lib/pg_eventstore/query_builders/partitions_filtering.rb +175 -51
  112. data/lib/pg_eventstore/query_builders/read_cursor/stream_cursor.rb +122 -0
  113. data/lib/pg_eventstore/query_builders/streams_global_index_filtering.rb +83 -0
  114. data/lib/pg_eventstore/query_builders/subscription_events_filtering.rb +79 -0
  115. data/lib/pg_eventstore/query_strategy/async.rb +64 -0
  116. data/lib/pg_eventstore/query_strategy/foreground.rb +25 -0
  117. data/lib/pg_eventstore/query_strategy.rb +19 -0
  118. data/lib/pg_eventstore/raw_event.rb +46 -0
  119. data/lib/pg_eventstore/rspec/test_helpers.rb +21 -8
  120. data/lib/pg_eventstore/sql_builder.rb +87 -8
  121. data/lib/pg_eventstore/stream.rb +13 -16
  122. data/lib/pg_eventstore/stream_global_index.rb +28 -0
  123. data/lib/pg_eventstore/subscriptions/callback_handlers/events_processor_handlers.rb +5 -17
  124. data/lib/pg_eventstore/subscriptions/callback_handlers/events_subscription_position_worker_handlers.rb +36 -0
  125. data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_feeder_handlers.rb +12 -0
  126. data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rb +21 -7
  127. data/lib/pg_eventstore/subscriptions/events_processor.rb +20 -18
  128. data/lib/pg_eventstore/subscriptions/events_processor_consumer/multiple.rb +63 -0
  129. data/lib/pg_eventstore/subscriptions/events_processor_consumer/replica.rb +68 -0
  130. data/lib/pg_eventstore/subscriptions/events_processor_consumer/single.rb +62 -0
  131. data/lib/pg_eventstore/subscriptions/events_processor_consumer.rb +44 -0
  132. data/lib/pg_eventstore/subscriptions/events_subscription_position_worker.rb +68 -0
  133. data/lib/pg_eventstore/subscriptions/queries/event_subscription_position_queries.rb +177 -0
  134. data/lib/pg_eventstore/subscriptions/queries/subscription_queries.rb +78 -54
  135. data/lib/pg_eventstore/subscriptions/replica_subscription_handler.rb +140 -0
  136. data/lib/pg_eventstore/subscriptions/replica_subscription_runner.rb +9 -0
  137. data/lib/pg_eventstore/subscriptions/subscription.rb +2 -1
  138. data/lib/pg_eventstore/subscriptions/subscription_feed_strategies/collection.rb +36 -0
  139. data/lib/pg_eventstore/subscriptions/subscription_feed_strategies/index_read_strategy.rb +82 -0
  140. data/lib/pg_eventstore/subscriptions/subscription_feed_strategies/replication_strategy.rb +75 -0
  141. data/lib/pg_eventstore/subscriptions/subscription_feed_strategy.rb +31 -0
  142. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +24 -0
  143. data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +4 -2
  144. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +20 -9
  145. data/lib/pg_eventstore/subscriptions/subscription_runner_commands/reset_position.rb +1 -1
  146. data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +9 -18
  147. data/lib/pg_eventstore/subscriptions/subscriptions_lifecycle.rb +0 -12
  148. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +74 -14
  149. data/lib/pg_eventstore/{subscriptions/synchronized_array.rb → synchronized_array.rb} +15 -0
  150. data/lib/pg_eventstore/tasks/setup.rake +2 -2
  151. data/lib/pg_eventstore/utils.rb +31 -0
  152. data/lib/pg_eventstore/version.rb +1 -1
  153. data/lib/pg_eventstore/web/application.rb +100 -16
  154. data/lib/pg_eventstore/web/paginator/base_collection.rb +5 -1
  155. data/lib/pg_eventstore/web/paginator/event_types_collection.rb +14 -4
  156. data/lib/pg_eventstore/web/paginator/events_collection.rb +29 -33
  157. data/lib/pg_eventstore/web/paginator/helpers.rb +16 -9
  158. data/lib/pg_eventstore/web/paginator/markers_collection.rb +48 -0
  159. data/lib/pg_eventstore/web/paginator/stream_contexts_collection.rb +13 -2
  160. data/lib/pg_eventstore/web/paginator/stream_ids_collection.rb +21 -11
  161. data/lib/pg_eventstore/web/paginator/stream_names_collection.rb +18 -4
  162. data/lib/pg_eventstore/web/paginator/streams_collection.rb +95 -0
  163. data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +49 -22
  164. data/lib/pg_eventstore/web/subscriptions/set_collection.rb +1 -1
  165. data/lib/pg_eventstore/web/subscriptions/subscriptions.rb +1 -1
  166. data/lib/pg_eventstore/web/subscriptions/with_state/set_collection.rb +1 -1
  167. data/lib/pg_eventstore/web/subscriptions/with_state/subscriptions.rb +1 -1
  168. data/lib/pg_eventstore/web/views/home/dashboard.erb +11 -9
  169. data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +18 -5
  170. data/lib/pg_eventstore/web/views/home/partials/events.erb +38 -1
  171. data/lib/pg_eventstore/web/views/home/partials/markers_filter.erb +18 -0
  172. data/lib/pg_eventstore/web/views/layouts/application.erb +6 -0
  173. data/lib/pg_eventstore/web/views/streams/index.erb +110 -0
  174. data/lib/pg_eventstore/web/views/streams/partials/streams.erb +11 -0
  175. data/lib/pg_eventstore/web/views/subscriptions/index.erb +4 -1
  176. data/lib/pg_eventstore/web.rb +2 -0
  177. data/lib/pg_eventstore.rb +23 -60
  178. data/pg_eventstore.gemspec +6 -4
  179. data/sig/interfaces/_raw_events_handler.rbs +3 -0
  180. data/sig/interfaces/subscription_handler.rbs +1 -1
  181. data/sig/pg/result.rbs +9 -0
  182. data/sig/pg_eventstore/async_runner.rbs +13 -0
  183. data/sig/pg_eventstore/basic_config.rbs +9 -0
  184. data/sig/pg_eventstore/chunks/chunk.rbs +13 -0
  185. data/sig/pg_eventstore/chunks/read_api_events_index_chunk.rbs +30 -0
  186. data/sig/pg_eventstore/chunks/replica_events_index_chunk.rbs +11 -0
  187. data/sig/pg_eventstore/chunks/repository.rbs +22 -0
  188. data/sig/pg_eventstore/chunks/subscription_checkpoint_chunk.rbs +18 -0
  189. data/sig/pg_eventstore/chunks/subscription_events_index_chunk.rbs +37 -0
  190. data/sig/pg_eventstore/client.rbs +40 -42
  191. data/sig/pg_eventstore/commands/append.rbs +11 -25
  192. data/sig/pg_eventstore/commands/event_modifiers/prepare_link_event.rbs +9 -12
  193. data/sig/pg_eventstore/commands/event_modifiers/prepare_regular_event.rbs +5 -4
  194. data/sig/pg_eventstore/commands/link_to.rbs +6 -11
  195. data/sig/pg_eventstore/commands/read.rbs +2 -5
  196. data/sig/pg_eventstore/commands/read_grouped.rbs +7 -0
  197. data/sig/pg_eventstore/commands/read_streams.rbs +7 -0
  198. data/sig/pg_eventstore/commands/read_streams_paginated.rbs +16 -0
  199. data/sig/pg_eventstore/commands/regular_stream_read_paginated.rbs +9 -17
  200. data/sig/pg_eventstore/commands/revision_check/current_revision.rbs +25 -0
  201. data/sig/pg_eventstore/commands/revision_check/event_type_revisions_comparison.rbs +14 -0
  202. data/sig/pg_eventstore/commands/revision_check/expected_revision.rbs +67 -0
  203. data/sig/pg_eventstore/commands/revision_check/stream_revision_check.rbs +20 -0
  204. data/sig/pg_eventstore/commands/revision_check/stream_revision_comparison.rbs +9 -0
  205. data/sig/pg_eventstore/commands/stream_revision.rbs +7 -0
  206. data/sig/pg_eventstore/commands/system_stream_read_paginated.rbs +9 -14
  207. data/sig/pg_eventstore/config.rbs +15 -6
  208. data/sig/pg_eventstore/connection.rbs +11 -18
  209. data/sig/pg_eventstore/errors.rbs +64 -34
  210. data/sig/pg_eventstore/event.rbs +16 -2
  211. data/sig/pg_eventstore/event_deserializer.rbs +5 -10
  212. data/sig/pg_eventstore/event_global_index.rbs +60 -0
  213. data/sig/pg_eventstore/event_marker.rbs +9 -0
  214. data/sig/pg_eventstore/event_marker_index.rbs +12 -0
  215. data/sig/pg_eventstore/extensions/acts_as_configurable.rbs +29 -0
  216. data/sig/pg_eventstore/extensions/options_defaults.rbs +7 -0
  217. data/sig/pg_eventstore/feature_marker.rbs +10 -0
  218. data/sig/pg_eventstore/maintenance.rbs +12 -8
  219. data/sig/pg_eventstore/middleware/event_trace.rbs +14 -0
  220. data/sig/pg_eventstore/partition.rbs +2 -4
  221. data/sig/pg_eventstore/pg_connection.rbs +8 -0
  222. data/sig/pg_eventstore/queries/event_marker_queries.rbs +21 -0
  223. data/sig/pg_eventstore/queries/event_queries.rbs +4 -36
  224. data/sig/pg_eventstore/queries/events_global_index_queries.rbs +37 -0
  225. data/sig/pg_eventstore/queries/index_filtering_queries.rbs +50 -0
  226. data/sig/pg_eventstore/queries/links_resolver.rbs +5 -9
  227. data/sig/pg_eventstore/queries/maintenance_queries.rbs +12 -9
  228. data/sig/pg_eventstore/queries/partition_queries.rbs +32 -67
  229. data/sig/pg_eventstore/queries/replica_queries.rbs +31 -0
  230. data/sig/pg_eventstore/queries/streams_global_index_queries.rbs +39 -0
  231. data/sig/pg_eventstore/queries/transaction_queries.rbs +2 -0
  232. data/sig/pg_eventstore/queries.rbs +11 -21
  233. data/sig/pg_eventstore/query_builders/basic_filtering.rbs +10 -3
  234. data/sig/pg_eventstore/query_builders/event_markers_filtering.rbs +17 -0
  235. data/sig/pg_eventstore/query_builders/event_markers_index_filtering.rbs +69 -0
  236. data/sig/pg_eventstore/query_builders/event_subscription_positions_filtering.rbs +15 -0
  237. data/sig/pg_eventstore/query_builders/events_filtering_query.rbs +9 -51
  238. data/sig/pg_eventstore/query_builders/events_global_index_filtering.rbs +68 -0
  239. data/sig/pg_eventstore/query_builders/filters/collection.rbs +84 -0
  240. data/sig/pg_eventstore/query_builders/filters/event_type_filter.rbs +19 -0
  241. data/sig/pg_eventstore/query_builders/filters/filter_row.rbs +21 -0
  242. data/sig/pg_eventstore/query_builders/filters/marker_filter.rbs +13 -0
  243. data/sig/pg_eventstore/query_builders/filters/marker_filter_row.rbs +19 -0
  244. data/sig/pg_eventstore/query_builders/filters/stream_filter.rbs +24 -0
  245. data/sig/pg_eventstore/query_builders/index_based_events_filtering.rbs +60 -0
  246. data/sig/pg_eventstore/query_builders/partitions_filtering.rbs +29 -10
  247. data/sig/pg_eventstore/query_builders/read_cursor/stream_cursor.rbs +47 -0
  248. data/sig/pg_eventstore/query_builders/streams_global_index_filtering.rbs +27 -0
  249. data/sig/pg_eventstore/query_builders/subscription_events_filtering.rbs +26 -0
  250. data/sig/pg_eventstore/query_strategy/async.rbs +17 -0
  251. data/sig/pg_eventstore/query_strategy/foreground.rbs +11 -0
  252. data/sig/pg_eventstore/query_strategy.rbs +7 -0
  253. data/sig/pg_eventstore/raw_event.rbs +20 -0
  254. data/sig/pg_eventstore/sql_builder.rbs +32 -10
  255. data/sig/pg_eventstore/stream.rbs +10 -18
  256. data/sig/pg_eventstore/stream_global_index.rbs +14 -0
  257. data/sig/pg_eventstore/subscriptions/callback_handlers/events_processor_handlers.rbs +5 -4
  258. data/sig/pg_eventstore/subscriptions/callback_handlers/events_subscription_position_worker_handlers.rbs +9 -0
  259. data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_feeder_handlers.rbs +17 -12
  260. data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rbs +17 -6
  261. data/sig/pg_eventstore/subscriptions/event_subscription_position_queries.rbs +30 -0
  262. data/sig/pg_eventstore/subscriptions/events_processor.rbs +12 -5
  263. data/sig/pg_eventstore/subscriptions/events_processor_consumer/multiple.rbs +17 -0
  264. data/sig/pg_eventstore/subscriptions/events_processor_consumer/replica.rbs +17 -0
  265. data/sig/pg_eventstore/subscriptions/events_processor_consumer/single.rbs +17 -0
  266. data/sig/pg_eventstore/subscriptions/events_processor_consumer.rbs +14 -0
  267. data/sig/pg_eventstore/subscriptions/events_subscription_position_worker.rbs +28 -0
  268. data/sig/pg_eventstore/subscriptions/queries/subscription_queries.rbs +7 -39
  269. data/sig/pg_eventstore/subscriptions/replica_subscription_handler.rbs +33 -0
  270. data/sig/pg_eventstore/subscriptions/replica_subscription_runner.rbs +4 -0
  271. data/sig/pg_eventstore/subscriptions/subscription_feed_strategy/collection.rbs +14 -0
  272. data/sig/pg_eventstore/subscriptions/subscription_feed_strategy/index_read_strategy.rbs +23 -0
  273. data/sig/pg_eventstore/subscriptions/subscription_feed_strategy/replication_strategy.rbs +23 -0
  274. data/sig/pg_eventstore/subscriptions/subscription_feed_strategy.rbs +11 -0
  275. data/sig/pg_eventstore/subscriptions/subscription_feeder.rbs +13 -10
  276. data/sig/pg_eventstore/subscriptions/subscription_handler_performance.rbs +1 -1
  277. data/sig/pg_eventstore/subscriptions/subscription_runner.rbs +13 -2
  278. data/sig/pg_eventstore/subscriptions/subscription_runners_feeder.rbs +1 -8
  279. data/sig/pg_eventstore/subscriptions/subscriptions_lifecycle.rbs +4 -6
  280. data/sig/pg_eventstore/subscriptions/subscriptions_manager.rbs +29 -43
  281. data/sig/pg_eventstore/synchronized_array.rbs +4 -0
  282. data/sig/pg_eventstore/utils.rbs +10 -11
  283. data/sig/pg_eventstore/web/application.rbs +13 -3
  284. data/sig/pg_eventstore/web/paginator/base_collection.rbs +2 -0
  285. data/sig/pg_eventstore/web/paginator/event_types_collection.rbs +4 -0
  286. data/sig/pg_eventstore/web/paginator/events_collection.rbs +11 -11
  287. data/sig/pg_eventstore/web/paginator/helpers.rbs +6 -15
  288. data/sig/pg_eventstore/web/paginator/markers_collection.rbs +13 -0
  289. data/sig/pg_eventstore/web/paginator/stream_contexts_collection.rbs +4 -0
  290. data/sig/pg_eventstore/web/paginator/stream_names_collection.rbs +4 -0
  291. data/sig/pg_eventstore/web/paginator/streams_collection.rbs +30 -0
  292. data/sig/pg_eventstore.rbs +8 -26
  293. metadata +175 -20
  294. data/Dockerfile +0 -3
  295. data/lib/pg_eventstore/commands/all_stream_read_grouped.rb +0 -69
  296. data/lib/pg_eventstore/commands/regular_stream_read_grouped.rb +0 -31
  297. data/lib/pg_eventstore/subscriptions/queries/subscription_service_queries.rb +0 -78
  298. data/lib/pg_eventstore/web/views/home/partials/system_stream_filter.erb +0 -15
  299. data/sig/pg_eventstore/commands/all_stream_read_grouped.rbs +0 -16
  300. data/sig/pg_eventstore/commands/regular_stream_read_grouped.rbs +0 -8
  301. data/sig/pg_eventstore/subscriptions/queries/subscription_service_queries.rbs +0 -19
  302. data/sig/pg_eventstore/subscriptions/synchronized_array.rbs +0 -4
  303. /data/sig/interfaces/{_raw_event_handler.rbs → raw_event_handler.rbs} +0 -0
@@ -0,0 +1,696 @@
1
+ # PgEventstore usage instructions for coding agents
2
+
3
+ Use these instructions when working in an application that has the `pg_eventstore` gem installed. Follow the
4
+ application's existing event, stream, configuration, and subscription conventions before introducing new ones.
5
+
6
+ ## Public API boundary
7
+
8
+ - Prefer the application-facing entry points `PgEventstore.configure`, `PgEventstore.client`,
9
+ `PgEventstore.maintenance`, and `PgEventstore.subscriptions_manager`.
10
+ - Use public value objects such as `PgEventstore::Event`, `PgEventstore::Stream`, `PgEventstore::Subscription`, and
11
+ `PgEventstore::SubscriptionsSet` only for their documented purposes.
12
+ - Treat every module or class whose source is marked `# @!visibility private` as an implementation detail. Do not
13
+ instantiate it, reference it from application code, mock it in tests, or depend on its behavior. This includes the
14
+ internal command, query, query-builder, chunk, serializer/deserializer, index, subscription runner/feeder, and web
15
+ implementation layers.
16
+ - Do not query or mutate pg_eventstore tables directly. The schema is partitioned and contains derived indexes and
17
+ subscription state that must stay consistent. Use the client, maintenance API, rake tasks, or CLI instead.
18
+ - When an unfamiliar constant appears in the gem source, check its YARD visibility and the bundled documentation
19
+ before using it. Being reachable as a Ruby constant does not make it public API.
20
+
21
+ ## Inspect the host application first
22
+
23
+ Before changing integration code, locate and reuse:
24
+
25
+ - the existing `PgEventstore.configure` block and named configuration, if any;
26
+ - the project's base event class, event naming/versioning convention, payload key convention, and stream factory;
27
+ - registered middleware and custom `event_class_resolver` behavior;
28
+ - wrappers around `PgEventstore.client`, concurrency/retry policy, and subscription process definitions;
29
+ - the dedicated eventstore database and test cleanup setup.
30
+
31
+ Do not create a second client abstraction or a competing stream naming scheme unless the project explicitly requires
32
+ one. Event type names and the `(context, stream_name, stream_id)` tuple are persisted contracts.
33
+
34
+ ## Installation and database setup
35
+
36
+ PgEventstore requires Ruby 3.3+, PostgreSQL 16+, a separate database, and PostgreSQL's `read committed` default
37
+ transaction isolation. A production deployment should normally use a separate PostgreSQL instance and a transaction
38
+ pooler such as PgBouncer.
39
+
40
+ For a new integration, add the setup tasks to the application's `Rakefile`:
41
+
42
+ ```ruby
43
+ load 'pg_eventstore/tasks/setup.rake'
44
+ ```
45
+
46
+ Then set `PG_EVENTSTORE_URI` and run:
47
+
48
+ ```bash
49
+ bundle exec rake pg_eventstore:create
50
+ bundle exec rake pg_eventstore:migrate
51
+ ```
52
+
53
+ Run migrations when upgrading the gem. `pg_eventstore:drop` deletes the entire eventstore database; never run it
54
+ against a non-disposable environment or without explicit authorization.
55
+
56
+ ## Configuration
57
+
58
+ Configure the gem during application boot and obtain clients through the facade:
59
+
60
+ ```ruby
61
+ require 'pg_eventstore'
62
+
63
+ PgEventstore.configure do |config|
64
+ config.pg_uri = ENV.fetch('PG_EVENTSTORE_URI')
65
+ config.connection_pool_size = 5
66
+ config.connection_pool_timeout = 5
67
+ end
68
+
69
+ client = PgEventstore.client
70
+ ```
71
+
72
+ Useful settings include `max_count`, `middlewares`, `event_class_resolver`, connection-pool settings, subscription
73
+ retry/timing settings, `eventstore_role`, and `max_events_to_replicate`. Assign settings inside `configure`; do not
74
+ mutate the returned frozen config later. Set `PgEventstore.logger` to the host application's logger when appropriate.
75
+
76
+ Named configurations represent different databases or behavior:
77
+
78
+ ```ruby
79
+ PgEventstore.configure(name: :regional_replica) do |config|
80
+ config.pg_uri = ENV.fetch('REGIONAL_EVENTSTORE_URI')
81
+ config.eventstore_role = :replica
82
+ end
83
+
84
+ client = PgEventstore.client(:regional_replica)
85
+ manager = PgEventstore.subscriptions_manager(:regional_replica, subscription_set: 'ReadModels')
86
+ ```
87
+
88
+ Always pass the intended name consistently. A `:replica` configuration cannot publish events. `:primary` and
89
+ `:replica` configurations cannot perform maintenance; only the default `:standalone` role can do both.
90
+
91
+ Size the connection pool for the process. A normal application process generally needs roughly one connection per
92
+ application thread. A subscription process may need up to:
93
+
94
+ ```ruby
95
+ number_of_subscriptions + (number_of_subscriptions / 10.0).ceil + 3
96
+ ```
97
+
98
+ ## Events and streams
99
+
100
+ Define domain events by subclassing `PgEventstore::Event` unless the project deliberately uses generic events:
101
+
102
+ ```ruby
103
+ class UserEmailChanged < PgEventstore::Event
104
+ end
105
+
106
+ event = UserEmailChanged.new(
107
+ data: { 'user_id' => user_id, 'email' => email },
108
+ metadata: { 'actor_id' => actor_id },
109
+ markers: ['user-profile']
110
+ )
111
+
112
+ stream = PgEventstore::Stream.new(
113
+ context: 'Identity',
114
+ stream_name: 'User',
115
+ stream_id: user_id.to_s
116
+ )
117
+ ```
118
+
119
+ - An event's default `type` is its Ruby class name. Keep persisted event types stable across renames and services. The
120
+ default resolver calls `Object.const_get(type)` and falls back to `PgEventstore::Event`; configure a resolver when
121
+ persisted types do not map directly to constants.
122
+ - Event `data` and `metadata` are JSON-backed and are read back with string keys. Prefer JSON-compatible values and the
123
+ key convention already used by the project.
124
+ - Event types beginning with `$` are reserved for system events.
125
+ - `id` defaults to a UUIDv7, but database-level uniqueness is not guaranteed for application-supplied IDs.
126
+ - `global_position`, `stream`, `stream_revision`, link fields, and `created_at` are persistence-assigned. Use the event
127
+ returned by the client when those fields are needed; do not try to set them manually.
128
+ - Treat `global_position` as an event identity/pagination coordinate, not a commit timestamp, consistency watermark, or
129
+ cross-stream causal order. `stream_revision` provides ordering within one stream; independent streams can allocate
130
+ global positions and commit in a different order because of PostgreSQL MVCC.
131
+ - `PgEventstore::Stream.all_stream` is a read scope over all streams, not a stream to append to.
132
+
133
+ ## Implementing business logic
134
+
135
+ Organize application behavior around three public-API workflows:
136
+
137
+ - **Command side:** read the facts needed for a decision, rebuild current state, and validate the command in domain
138
+ code. For a standalone append, use an expected revision that covers every inspected fact. For a cross-stream rule,
139
+ put the minimal read, decision, and appends inside `client.multiple` instead.
140
+ - **Query side:** filter and fold events into a query-shaped view. Compute it on demand only when a best-effort live
141
+ result is acceptable. Maintain it with a subscription when the projection must reliably converge after processing
142
+ every matching event.
143
+ - **Reactions:** subscribe to events and idempotently update a read model or issue another command. Subscription
144
+ processing is asynchronous and at least once, so do not use it where the originating command requires immediate
145
+ consistency.
146
+
147
+ Keep decision code separate from storage code. A domain object or pure function should accept events/current state and
148
+ return new events or a domain error; the application layer should perform reads, retries, and appends. Do not put mutable
149
+ domain state in `PgEventstore::Event` subclasses.
150
+
151
+ ### Choose the consistency boundary deliberately
152
+
153
+ The consistency mechanism must cover the same set of facts that the business rule inspected:
154
+
155
+ - Use a **stream revision** when every event in one stream can affect the decision. This is the conventional aggregate
156
+ boundary and makes any concurrent change to that stream invalidate the stale decision.
157
+ - Use **Dynamic Consistency Boundaries** when only selected event types, or selected types carrying a marker, affect
158
+ the rule. Unrelated appends to the same stream can then proceed without false conflicts.
159
+ - Use `client.multiple` when one invariant spans streams and its filtered read plus authorized writes must be
160
+ immediately consistent, or when several streams in the same configured database must change atomically. It provides
161
+ one serializable transaction across them. Keep the transactional working set small: include every read and write
162
+ involved in the invariant, but no unrelated history or commands. Do not add expected-revision locks inside the
163
+ block; every execution must reread the facts and remake the business decision.
164
+ - Use a dedicated coordination/registry stream when a cross-entity invariant needs one authoritative lock boundary.
165
+ For example, email claims can be events in a registry stream and scoped with a stable, opaque claim marker.
166
+ - Use subscriptions when eventual consistency is acceptable.
167
+
168
+ The domain function validates whether the proposed event is allowed. Outside `client.multiple`, expected revisions make
169
+ that validation safe by atomically rejecting the append if any covered fact changed after it was read; they do not
170
+ replace the business rule. A DCB check applies to the stream receiving the append. Reading the all stream and then
171
+ appending to an unrelated stream does not make a cross-stream rule atomic unless both operations execute inside
172
+ `client.multiple`. Never choose a boundary narrower than the business rule: an omitted event type or marker cannot
173
+ participate in conflict detection.
174
+
175
+ ### Whole-stream aggregate pattern
176
+
177
+ For an aggregate whose state depends on its complete history, load all pages, fold the events, decide, and append
178
+ against the revision that was folded:
179
+
180
+ ```ruby
181
+ class MoneyWithdrawn < PgEventstore::Event
182
+ end
183
+
184
+ def account_state(events)
185
+ events.each_with_object({ balance_cents: 0, closed: false }) do |event, state|
186
+ case event.type
187
+ when 'AccountOpened'
188
+ state[:balance_cents] = event.data.fetch('opening_balance_cents')
189
+ when 'MoneyDeposited'
190
+ state[:balance_cents] += event.data.fetch('amount_cents')
191
+ when 'MoneyWithdrawn'
192
+ state[:balance_cents] -= event.data.fetch('amount_cents')
193
+ when 'AccountClosed'
194
+ state[:closed] = true
195
+ end
196
+ end
197
+ end
198
+
199
+ def decide_withdrawal(state, amount_cents)
200
+ raise AccountClosed if state[:closed]
201
+ raise InvalidAmount unless amount_cents.positive?
202
+ raise InsufficientFunds if state[:balance_cents] < amount_cents
203
+
204
+ MoneyWithdrawn.new(data: { 'amount_cents' => amount_cents })
205
+ end
206
+
207
+ def load_history(client, stream)
208
+ client.read_paginated(stream, options: { direction: :asc }).flat_map(&:itself)
209
+ rescue PgEventstore::StreamNotFoundError
210
+ []
211
+ end
212
+
213
+ def withdraw(client, account_id, amount_cents)
214
+ stream = PgEventstore::Stream.new(
215
+ context: 'Banking',
216
+ stream_name: 'Account',
217
+ stream_id: account_id.to_s
218
+ )
219
+ attempts = 0
220
+
221
+ begin
222
+ history = load_history(client, stream)
223
+ state = account_state(history)
224
+ event = decide_withdrawal(state, amount_cents)
225
+ expected_revision = history.empty? ? :no_stream : history.last.stream_revision
226
+
227
+ client.append_to_stream(stream, event, options: { expected_revision: })
228
+ rescue PgEventstore::WrongExpectedRevisionError
229
+ attempts += 1
230
+ raise if attempts >= 3
231
+
232
+ # Reload, rebuild, and make the decision again; do not merely retry the stale event.
233
+ retry
234
+ end
235
+ end
236
+ ```
237
+
238
+ Keep reducers and decision functions deterministic: no database calls, current-time reads, random choices, or external
239
+ effects. They must produce the same state/decision from the same history. Handle old payload shapes deliberately, and
240
+ ignore an event type only when it truly cannot affect the rule.
241
+
242
+ Do not use a single `read` call to rebuild an unbounded aggregate: it stops at `max_count`. Use `read_paginated`, or use
243
+ snapshots implemented by the host application and then replay events after the snapshot revision. A snapshot is a
244
+ cache; the event history and expected revision remain authoritative. Start replay at `snapshot_revision + 1`, then
245
+ append against the last replayed revision, or against the snapshot revision when no newer event exists.
246
+
247
+ ### Dynamic Consistency Boundary pattern
248
+
249
+ DCBs reduce unnecessary optimistic-concurrency conflicts in a stream containing several independently changing
250
+ subjects. In this example a board is one stream, while each card is identified by a stable marker. Renaming one card
251
+ must conflict with creation, rename, or archival of that card, but not with changes to another card:
252
+
253
+ ```ruby
254
+ CARD_DECISION_TYPES = %w[CardCreated CardTitleChanged CardArchived].freeze
255
+
256
+ def rename_card(client, board_id:, card_id:, title:)
257
+ stream = PgEventstore::Stream.new(
258
+ context: 'Planning',
259
+ stream_name: 'Board',
260
+ stream_id: board_id.to_s
261
+ )
262
+ boundary_marker = "card:#{card_id}"
263
+ filters = CARD_DECISION_TYPES.map do |type|
264
+ { type:, markers: [boundary_marker] }
265
+ end
266
+
267
+ latest_events = client.read_grouped(
268
+ stream,
269
+ options: { direction: :desc, filter: { event_types: filters } }
270
+ )
271
+ latest_by_type = latest_events.to_h { |event| [event.type, event] }
272
+
273
+ raise CardNotFound unless latest_by_type['CardCreated']
274
+ raise CardAlreadyArchived if latest_by_type['CardArchived']
275
+
276
+ expected_revision = CARD_DECISION_TYPES.to_h do |type|
277
+ observed = latest_by_type[type]
278
+ revision = observed ? observed.stream_revision : :no_event
279
+ [type, { expected_revision: revision, markers: [boundary_marker] }]
280
+ end
281
+
282
+ event = CardTitleChanged.new(
283
+ data: { 'card_id' => card_id, 'title' => title },
284
+ markers: [boundary_marker]
285
+ )
286
+ client.append_to_stream(stream, event, options: { expected_revision: })
287
+ rescue PgEventstore::StreamNotFoundError
288
+ raise CardNotFound
289
+ end
290
+ ```
291
+
292
+ If two processes rename the same card from the same observed state, one append succeeds and the other raises
293
+ `PgEventstore::WrongExpectedTypesRevisionError`. A change carrying another card's marker does not conflict. Catch that
294
+ error at the application boundary and repeat the entire read/validate/append operation with a retry limit.
295
+
296
+ Use `read_grouped(direction: :desc)` only when the latest event of each relevant type is sufficient to decide. If the
297
+ rule depends on every occurrence, use a filtered `read_paginated` fold and take the most recent observed revision for
298
+ each type/marker boundary. Put the same stable boundary marker on every event that participates in the rule. Markers
299
+ are persisted and indexed, so keep them non-secret and purposeful; marker lists use OR semantics.
300
+
301
+ ### Cross-stream consistency with `client.multiple`
302
+
303
+ When one business invariant genuinely spans streams, `client.multiple` is the powerful consistency mechanism to use.
304
+ It makes the minimal fact read, decision, and one or more necessary appends part of one serializable operation;
305
+ per-stream expected revisions and DCBs cannot by themselves protect facts held in other streams. A bare all-stream
306
+ projection followed by a separate append cannot protect such an invariant.
307
+
308
+ Keep the serializable unit small without weakening the invariant. "Small" describes the events read and changed for
309
+ one decision, not the number of streams across which the invariant may range. For example, users have separate
310
+ streams, but a username must be unique across all of them. The query below is deliberately narrow: one context/name,
311
+ one event type, one indexed marker, and at most one row:
312
+
313
+ ```ruby
314
+ class UsernameClaimed < PgEventstore::Event
315
+ end
316
+
317
+ def claim_username(client, user_id:, username:, username_key:)
318
+ user_stream = PgEventstore::Stream.new(
319
+ context: 'Identity',
320
+ stream_name: 'User',
321
+ stream_id: user_id.to_s
322
+ )
323
+ marker = "username:#{username_key}" # A stable, non-secret/opaque lookup key.
324
+
325
+ client.multiple do
326
+ existing_claim = client.read(
327
+ PgEventstore::Stream.all_stream,
328
+ options: {
329
+ max_count: 1,
330
+ filter: {
331
+ streams: [{ context: 'Identity', stream_name: 'User' }],
332
+ event_types: [{ type: 'UsernameClaimed', markers: [marker] }]
333
+ }
334
+ }
335
+ ).first
336
+ raise UsernameTaken if existing_claim
337
+
338
+ event = UsernameClaimed.new(
339
+ data: { 'user_id' => user_id, 'username' => username },
340
+ markers: [marker]
341
+ )
342
+ client.append_to_stream(user_stream, event)
343
+ end
344
+ end
345
+ ```
346
+
347
+ `client.multiple` uses PostgreSQL `SERIALIZABLE` isolation. The read and append therefore have an outcome equivalent to
348
+ some serial execution. If concurrent username claims create a serialization conflict, PostgreSQL aborts one transaction
349
+ and pg_eventstore reruns its entire block; the retried block then observes the winning claim and raises `UsernameTaken`.
350
+
351
+ This is the event-store form of `SELECT` followed conditionally by `INSERT`: read the exact fact, return or raise when it
352
+ already exists, and append only when it does not. Both steps must happen inside the block on every execution. Do not
353
+ load state, construct a supposedly final event, or capture a revision before entering `multiple`, because a retry must
354
+ base the business decision on its own fresh reads.
355
+
356
+ Do not pass `expected_revision` to an append inside `multiple`. It adds no protection for facts read by the same
357
+ serializable block. When concurrency invalidates those reads, the transaction conflict causes pg_eventstore to rerun
358
+ the whole block; its control flow must then reach the correct result from the events it now reads. A caller-supplied
359
+ revision check is not a substitute for that retriable business logic and can turn a valid rerun into a revision error.
360
+
361
+ The precise guarantee is serial equivalence, not that every concurrent append literally causes a retry. PostgreSQL may
362
+ validly order the block before a concurrent transaction. The published event is consistent with that serial order, but
363
+ is not necessarily based on every transaction that physically commits while the block is running.
364
+
365
+ Serializable transactions are resource-intensive. Keep this pattern selective, bounded, and short: use exact stream,
366
+ event-type, and marker filters, read only the facts required by the invariant, and mutate only the necessary streams.
367
+ Appending to more than one stream is appropriate when those writes must be atomic for the same invariant; unrelated
368
+ events and commands belong outside the block.
369
+
370
+ Do not put an unbounded `read_paginated` replay, dashboard/report projection, broad all-stream fold, or large set of
371
+ partitions/streams inside `multiple`. Model a narrower registry/coordination stream or marker boundary when possible;
372
+ use a subscription when a broad projection can be eventually consistent.
373
+
374
+ If a genuinely bounded paginated read is unavoidable, fully consume its lazy enumerator inside the `multiple` block;
375
+ consuming it afterward performs the reads outside the transaction. The block can rerun, so keep its
376
+ projection/decision deterministic and move HTTP calls, emails, jobs, and other external effects outside. This pattern
377
+ makes a small synchronous decision consistent; it does not create a durable projection position like a subscription
378
+ does.
379
+
380
+ ## Appending and concurrency
381
+
382
+ Use `append_to_stream`; it returns the persisted event, or an array of persisted events:
383
+
384
+ ```ruby
385
+ persisted_event = client.append_to_stream(
386
+ stream,
387
+ event,
388
+ options: { expected_revision: :no_stream }
389
+ )
390
+ ```
391
+
392
+ Passing an array appends all events atomically and in order. Use optimistic concurrency for business writes:
393
+
394
+ - `:any` performs no check and is the default;
395
+ - `:no_stream` requires the stream not to exist;
396
+ - `:stream_exists` requires it to exist;
397
+ - an integer requires the current stream revision to equal that value.
398
+
399
+ `client.stream_revision(stream)` is the efficient revision lookup and returns
400
+ `PgEventstore::Stream::NON_EXISTING_STREAM_REVISION` (`-1`) for a missing stream. When state was built by reading a
401
+ stream, normally use the last event's `stream_revision` for the subsequent append.
402
+
403
+ Handle `PgEventstore::WrongExpectedRevisionError` by re-reading the stream, rebuilding state, rechecking business
404
+ rules, and then making a bounded retry. Do not blindly retry the same stale write. Per-event-type Dynamic Consistency
405
+ Boundaries are available by passing a type-to-revision map and raise
406
+ `PgEventstore::WrongExpectedTypesRevisionError` on mismatch:
407
+
408
+ ```ruby
409
+ client.append_to_stream(
410
+ stream,
411
+ event,
412
+ options: {
413
+ expected_revision: {
414
+ 'UserCreated' => :event_exists,
415
+ 'UserEmailChanged' => { expected_revision: 3, markers: ['verified'] }
416
+ }
417
+ }
418
+ )
419
+ ```
420
+
421
+ Per-type revision values are `:any`, `:no_event`, `:event_exists`, or an integer. Marker lists have OR semantics, not
422
+ AND semantics; introduce a combined marker when an AND-like distinction is required.
423
+
424
+ Expected-revision locks belong to the standalone read/decide/append workflow. Omit them inside `client.multiple`; let
425
+ the serializable transaction retry the complete in-block read and decision instead.
426
+
427
+ Use `client.multiple` when commands against the same configured eventstore must share one PostgreSQL `SERIALIZABLE`
428
+ transaction:
429
+
430
+ ```ruby
431
+ client.multiple do
432
+ client.append_to_stream(first_stream, first_event)
433
+ client.append_to_stream(second_stream, second_event)
434
+ end
435
+ ```
436
+
437
+ Keep the block small. It may be executed more than once because of concurrent schema/index creation or transaction
438
+ retries. Put emails, jobs, HTTP calls, and other non-idempotent side effects outside the block. Use
439
+ `multiple(read_only: true)` only for read-only commands; writes then raise `PG::ReadOnlySqlTransaction`.
440
+
441
+ ## Reading
442
+
443
+ Read a specific stream or the all-stream scope through the client:
444
+
445
+ ```ruby
446
+ events = client.read(
447
+ stream,
448
+ options: {
449
+ direction: :asc,
450
+ from_revision: 0,
451
+ max_count: 100,
452
+ filter: { event_types: ['UserEmailChanged'] }
453
+ }
454
+ )
455
+ ```
456
+
457
+ - A missing specific stream raises `PgEventstore::StreamNotFoundError`; decide explicitly whether that means empty
458
+ state or an application error.
459
+ - Use `from_revision`/`to_revision` for a specific stream and `from_position`/`to_position` for
460
+ `PgEventstore::Stream.all_stream`.
461
+ - `direction` accepts `:asc`/`:desc` as well as the documented string forms.
462
+ - `resolve_link_tos: true` returns original events when reading a projection stream containing links.
463
+ - Filters use `event_types`, whose entries may be type strings or hashes such as
464
+ `{ type: 'UserEmailChanged', markers: ['verified'] }`. On the all stream, `streams` entries must contain `context`,
465
+ `context` plus `stream_name`, or all three stream attributes. Do not rely on invalid partial filters being rejected;
466
+ some are ignored.
467
+ - Marker alternatives are ORed. On all-stream reads, a marker-only filter combined with only `context`, or with
468
+ `context` and `stream_name`, must also specify an event type.
469
+
470
+ Use `read_paginated` for unbounded histories. It returns an enumerator that yields arrays, not individual events:
471
+
472
+ ```ruby
473
+ client.read_paginated(PgEventstore::Stream.all_stream, options: { max_count: 500 }).each do |batch|
474
+ batch.each { |event| project(event) }
475
+ end
476
+ ```
477
+
478
+ Use `read_grouped` when the requirement is one oldest or newest event per event type. Its `max_count` option is ignored.
479
+ On the all stream, equal event types are distinguished by their `context`/`stream_name` pair, not by `stream_id`; do
480
+ not use it to fetch one latest event per entity across otherwise identical entity streams. Use `read_streams` and
481
+ `read_streams_paginated` to enumerate streams; paginated stream reads also yield arrays.
482
+
483
+ ## Building projections and query models
484
+
485
+ A projection function can be a deterministic fold from selected events to query-shaped state, but that does not make
486
+ every way of supplying its input reliable or idempotent. Keep the fold pure enough to replay, handle every historical
487
+ event version that can be returned, and test it in per-stream revision order with plausible cross-stream interleavings.
488
+ Select the implementation based on the required guarantees:
489
+
490
+ - Build an **on-demand projection** with `read`/`read_paginated` when the filtered history is bounded, the query is
491
+ infrequent, and a potentially incomplete best-effort result during concurrent writes is acceptable. The Read API has
492
+ no durable projection position and does not guarantee a gap-free all-stream scan.
493
+ - Use `read_grouped` when the projection needs only the oldest or newest event of each type, not accumulated history.
494
+ - Maintain a **link stream** when several consumers need the same ordered subset of existing events without copying
495
+ their payloads.
496
+ - Maintain an external **materialized view** with a subscription when queries are frequent, histories are large, or
497
+ the view must reliably converge by eventually processing every matching event.
498
+
499
+ This differs from command-side validation. Outside `client.multiple`, a command read is followed by an
500
+ expected-revision check that atomically rejects a decision based on stale covered facts. Inside `multiple`, the minimal
501
+ filtered read, decision, and writes are protected by serializable transaction retries instead. A standalone on-demand
502
+ query fold has neither validation mechanism. Do not use an all-stream on-demand projection as the authority for
503
+ invariants, exact financial totals, uniqueness, or other decisions that require a complete event set.
504
+
505
+ ### On-demand (on-the-fly) filtered projection
506
+
507
+ The all-stream read API can combine stream scope, event types, markers, and direction. For example, events in every
508
+ order stream can carry a `customer:<id>` marker, allowing a customer summary to be computed without loading unrelated
509
+ orders:
510
+
511
+ ```ruby
512
+ ORDER_OVERVIEW_TYPES = %w[OrderPlaced OrderCancelled OrderShipped].freeze
513
+
514
+ def customer_order_overview(client, customer_id)
515
+ customer_marker = "customer:#{customer_id}"
516
+ options = {
517
+ direction: :asc,
518
+ max_count: 500,
519
+ filter: {
520
+ streams: [{ context: 'Sales', stream_name: 'Order' }],
521
+ event_types: ORDER_OVERVIEW_TYPES.map do |type|
522
+ { type:, markers: [customer_marker] }
523
+ end
524
+ }
525
+ }
526
+ overview = { orders: {} }
527
+ client.read_paginated(PgEventstore::Stream.all_stream, options:).each do |batch|
528
+ batch.each do |event|
529
+ order_id = event.data.fetch('order_id')
530
+
531
+ case event.type
532
+ when 'OrderPlaced'
533
+ overview[:orders][order_id] = {
534
+ status: :placed,
535
+ total_cents: event.data.fetch('total_cents')
536
+ }
537
+ when 'OrderCancelled'
538
+ order = (overview[:orders][order_id] ||= { total_cents: nil })
539
+ order[:status] = :cancelled
540
+ when 'OrderShipped'
541
+ order = (overview[:orders][order_id] ||= { total_cents: nil })
542
+ order[:status] = :shipped
543
+ end
544
+ end
545
+ end
546
+ overview
547
+ end
548
+ ```
549
+
550
+ The overview tolerates a status event whose earlier placement event was absent from this live pass, leaving
551
+ `total_cents` unknown. That is appropriate only because the result is explicitly non-authoritative and can be rebuilt.
552
+
553
+ This example repeats the marker constraint for each event type because an all-stream marker-only filter combined with
554
+ `context` and `stream_name` is not supported. Stream filters and event-type filters form unions of their matching
555
+ combinations; design the filter and test it against events that must both match and not match.
556
+
557
+ Use `read_paginated` directly, but treat the result as a best-effort live view. Its pages are not one MVCC snapshot and
558
+ the Read API stores no durable progress for the projection. An independent transaction can allocate a smaller
559
+ `global_position`, remain uncommitted while a higher position becomes visible, and commit after the pagination cursor
560
+ has advanced. That event may be absent from the current pass and appear only when the projection is rebuilt later.
561
+ Re-running the fold after writers settle can produce a complete result, but one live execution has no such guarantee.
562
+
563
+ Adding `to_position` does not fix this: it restricts numeric positions but does not create a commit-order watermark or
564
+ a stable point-in-time view. Use `from_position`/`to_position` only when the requirement is explicitly about a known
565
+ numeric position range. Do not infer causality or business order between different streams from `global_position`;
566
+ make a cross-stream fold insensitive to interleaving, or encode the required relationship in domain events, links, or
567
+ causation metadata. For a durable projection that must eventually process every matching event, use a subscription and
568
+ an idempotent handler.
569
+
570
+ ### Materialized projection
571
+
572
+ When a projection must reliably converge, define a subscription with the same filters and update a query database in
573
+ the handler. Subscriptions use deterministic subscription positions and persist progress separately from
574
+ `global_position`, allowing a healthy/recoverable subscription to process every matching event eventually while
575
+ preserving revision order within a stream. Delivery is still at least once: make the update idempotent using a source
576
+ identity such as the source config plus `event.global_position`, or the event's stream tuple plus `stream_revision`.
577
+ Advance application-visible state in the same database transaction as that idempotency record where possible. A failed
578
+ handler or batch can run again.
579
+
580
+ Treat the materialized view as rebuildable derived data. Keep its projection function usable by both the live
581
+ subscription and a replay task, and store enough schema/version information to evolve old event payloads. If immediate
582
+ read-after-write behavior is required for one known stream, read that stream directly. Do not expect either an
583
+ all-stream on-demand fold or a materialized view to provide an immediate cross-stream snapshot.
584
+
585
+ ## Linking events
586
+
587
+ Use links for projection streams. Only persisted events can be linked:
588
+
589
+ ```ruby
590
+ persisted = client.append_to_stream(source_stream, event)
591
+ client.link_to(projection_stream, persisted, options: { expected_revision: :no_stream })
592
+
593
+ projected_events = client.read(projection_stream, options: { resolve_link_tos: true })
594
+ ```
595
+
596
+ `link_to` accepts one event or an array and supports the same stream-level concurrency options as appending. Unlike
597
+ normal append/read operations, link events use no configured middleware by default; pass middleware keys explicitly if
598
+ needed. Deleting an original event or stream does not automatically delete links to it.
599
+
600
+ ## Subscriptions
601
+
602
+ Create subscriptions through a manager and give the set and each subscription a stable, application-specific name:
603
+
604
+ ```ruby
605
+ manager = PgEventstore.subscriptions_manager(subscription_set: 'IdentityReadModels')
606
+ manager.subscribe(
607
+ 'ProjectUsers',
608
+ handler: ->(event) { UserProjector.call(event) },
609
+ options: {
610
+ filter: {
611
+ streams: [{ context: 'Identity', stream_name: 'User' }],
612
+ event_types: %w[UserCreated UserEmailChanged]
613
+ }
614
+ }
615
+ )
616
+ manager.start
617
+ ```
618
+
619
+ For a dedicated process, put configuration and definitions in a Ruby file and start it with:
620
+
621
+ ```bash
622
+ pg-eventstore subscriptions start -r ./subscriptions.rb
623
+ ```
624
+
625
+ - Subscription names must be unique within a set. Treat set/name pairs as persistent identities because positions and
626
+ locks are stored under them.
627
+ - Run a given subscription set in only one manager/process. Do not use `force_lock: true` as routine failover; it can
628
+ cause duplicate processing and broken positions. Use `start!` when lock failure must raise; `start` logs the lock
629
+ error and returns `nil`.
630
+ - Treat delivery as at least once. Handlers must be idempotent. If `in_batches: true` is used, the handler receives an
631
+ array and a failed batch is delivered again in full.
632
+ - Subscription `options[:from_position]` is an exclusive subscription position, not
633
+ `Event#global_position`. Subscription ordering can differ from global-position ordering, although revision order is
634
+ preserved within one stream.
635
+ - Subscription filters follow all-stream filter syntax, but additionally allow marker-only filters with partial stream
636
+ scopes and type-prefix entries such as `{ prefix: 'User' }`.
637
+ - Per-subscription retry, interval, notifier, shutdown, middleware, and batching options may override configuration.
638
+ Keep CPU-heavy subscriptions limited per process and size the connection pool accordingly.
639
+
640
+ ## Middleware and event tracing
641
+
642
+ Middleware mutates events before persistence and after reads/subscription delivery. Include `PgEventstore::Middleware`
643
+ or provide an object responding to both `serialize(event)` and `deserialize(event)`, then register named instances:
644
+
645
+ ```ruby
646
+ class RedactSecrets
647
+ include PgEventstore::Middleware
648
+
649
+ def serialize(event)
650
+ # Mutate the event in place.
651
+ end
652
+
653
+ def deserialize(event)
654
+ # Restore or transform the event in place.
655
+ end
656
+ end
657
+
658
+ PgEventstore.configure do |config|
659
+ config.middlewares = { redact_secrets: RedactSecrets.new }
660
+ end
661
+ ```
662
+
663
+ Commands can retry, so both methods must be deterministic and retry-safe, and any external effects must be idempotent.
664
+ Append, read, and subscription calls use all configured middleware by default; passing `middlewares: [:name]` selects
665
+ only those entries. Links are the exception and default to none.
666
+
667
+ To opt into causation/correlation tracing, register `PgEventstore::Middleware::EventTracing` and pass a persisted event
668
+ as `caused_by:` when creating the next event. The middleware then assigns `causation_id`, propagates or creates
669
+ `correlation_id`, and indexes the identifiers as feature markers.
670
+
671
+ ## Maintenance, replication, and tests
672
+
673
+ - Use `PgEventstore.maintenance.delete_stream(stream)` or `delete_event(event)` only for explicit maintenance needs,
674
+ not ordinary domain behavior. Both return `false` when the target is missing. Deleting an early event renumbers later
675
+ revisions and can lock many rows; `force: true` bypasses the safety limit and must be used deliberately. Any delete
676
+ operation may require further, deeper maintenance actions, such as reindexing related indexes, thus avoid maintenance
677
+ API at all cost. Do not use it in any business logic.
678
+ - Configure replication through named `:primary` and `:replica` configs and the public
679
+ `manager.create_replication` API. Do not attempt to copy selected internal tables manually.
680
+ - For RSpec integration tests, require `pg_eventstore/rspec/test_helpers` and call
681
+ `PgEventstore::TestHelpers.clean_up_db` (or pass a named test config). This helper is destructive: point it only at a
682
+ dedicated test database, and do not run concurrent examples that clean the same database.
683
+ - Test through the public facade. Cover optimistic-concurrency conflicts, missing streams, event round trips,
684
+ middleware retry safety, and subscription-handler idempotency when those behaviors matter.
685
+
686
+ For edge cases and complete option lists, consult the gem's bundled chapters on
687
+ - [configuration](https://raw.githubusercontent.com/yousty/pg_eventstore/refs/heads/main/docs/configuration.md)
688
+ - [events and streams](https://raw.githubusercontent.com/yousty/pg_eventstore/refs/heads/main/docs/events_and_streams.md)
689
+ - [appending](https://raw.githubusercontent.com/yousty/pg_eventstore/refs/heads/main/docs/appending_events.md)
690
+ - [reading](https://raw.githubusercontent.com/yousty/pg_eventstore/refs/heads/main/docs/reading_events.md)
691
+ - [linking](https://raw.githubusercontent.com/yousty/pg_eventstore/refs/heads/main/docs/linking_events.md)
692
+ - [subscriptions](https://raw.githubusercontent.com/yousty/pg_eventstore/refs/heads/main/docs/subscriptions.md)
693
+ - [middleware](https://raw.githubusercontent.com/yousty/pg_eventstore/refs/heads/main/docs/writing_middleware.md)
694
+ - [maintenance](https://raw.githubusercontent.com/yousty/pg_eventstore/refs/heads/main/docs/maintenance.md)
695
+ - [multiple commands](https://raw.githubusercontent.com/yousty/pg_eventstore/refs/heads/main/docs/multiple_commands.md)
696
+ - [replication](https://raw.githubusercontent.com/yousty/pg_eventstore/refs/heads/main/docs/replication.md)