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
@@ -32,7 +32,7 @@ module PgEventstore
32
32
  # @return [Array<Hash>, nil]
33
33
  # rubocop:disable Style/HashConversion
34
34
  def streams_filter
35
- streams = QueryBuilders::EventsFiltering.extract_streams_filter(params)
35
+ streams = extract_streams_filter(params)
36
36
  streams = streams.select { _1 in { context: String, stream_name: String, stream_id: String } }
37
37
  streams = streams.map do |stream_attrs|
38
38
  Hash[stream_attrs.reject { |_, value| value == '' }].transform_keys(&:to_sym)
@@ -41,17 +41,32 @@ module PgEventstore
41
41
  end
42
42
  # rubocop:enable Style/HashConversion
43
43
 
44
- # @return [String, nil]
45
- def system_stream
46
- params in { filter: { system_stream: String => system_stream } }
47
- system_stream if Stream::KNOWN_SYSTEM_STREAMS.include?(system_stream)
48
- end
49
-
50
- # @return [Array<String>, nil]
44
+ # @return [Array<String, Hash>]
51
45
  def events_filter
52
46
  event_filters = { filter: { event_types: params.dig(:filter, :events) } }
53
- events = QueryBuilders::EventsFiltering.extract_event_types_filter(event_filters)
54
- events.reject { _1 == '' }
47
+ event_types = extract_event_types_filter(event_filters)
48
+ event_types.map do |filter|
49
+ case filter
50
+ when String
51
+ next if filter == ''
52
+ when Hash
53
+ next if filter[:type] == ''
54
+
55
+ filter[:markers] = normalize_markers(filter)
56
+ else
57
+ next
58
+ end
59
+
60
+ filter
61
+ end.compact
62
+ end
63
+
64
+ # @return [Array<String>]
65
+ def markers_filter
66
+ params in { filter: Hash => filter }
67
+ return [] unless filter
68
+
69
+ normalize_markers(filter)
55
70
  end
56
71
 
57
72
  # @return [Symbol]
@@ -137,13 +152,55 @@ module PgEventstore
137
152
  def unescape_empty_string(string)
138
153
  string == EMPTY_STRING_SIGN ? '' : string
139
154
  end
155
+
156
+ # @param options [Hash]
157
+ # @return [Array<String, Hash>]
158
+ def extract_event_types_filter(options)
159
+ options in { filter: { event_types: Array => event_types } }
160
+ event_types = event_types&.select do |event_type|
161
+ event_type.is_a?(String) || (event_type in { type: String })
162
+ end
163
+ event_types || []
164
+ end
165
+
166
+ # @param options [Hash]
167
+ # @return [Array<Hash[Symbol, String]>]
168
+ def extract_streams_filter(options)
169
+ options in { filter: { streams: Array => streams } }
170
+ streams = streams&.map do |stream_attrs|
171
+ stream_attrs in { context: String | NilClass => context }
172
+ stream_attrs in { stream_name: String | NilClass => stream_name }
173
+ stream_attrs in { stream_id: String | NilClass => stream_id }
174
+ { context:, stream_name:, stream_id: }
175
+ end
176
+ streams || []
177
+ end
178
+
179
+ private
180
+
181
+ # @param hash [Hash]
182
+ # @return [Array<String>]
183
+ def normalize_markers(hash)
184
+ hash in { markers: Array => markers }
185
+ markers ||= []
186
+ markers.grep(String).reject { _1 == '' }
187
+ end
140
188
  end
141
189
 
142
190
  get '/' do
143
191
  streams_filter = self.streams_filter&.map do |attrs|
144
192
  attrs.transform_values { unescape_empty_string(_1) }
145
193
  end
146
- events_filter = self.events_filter&.map(&method(:unescape_empty_string))
194
+ events_filter = self.events_filter.map do |event_type|
195
+ next unescape_empty_string(event_type) if event_type.is_a?(String)
196
+
197
+ event_type[:type] = unescape_empty_string(event_type[:type])
198
+ event_type[:markers] = event_type[:markers]&.grep(String) || []
199
+ event_type
200
+ end
201
+ markers_filter = self.markers_filter.map(&method(:unescape_empty_string))
202
+ events_filter.push({ markers: markers_filter }) if markers_filter.any?
203
+
147
204
  @collection = Paginator::EventsCollection.new(
148
205
  current_config,
149
206
  starting_id: params[:starting_id]&.to_i,
@@ -152,14 +209,20 @@ module PgEventstore
152
209
  options: {
153
210
  filter: { event_types: events_filter, streams: streams_filter },
154
211
  resolve_link_tos: resolve_link_tos?,
155
- },
156
- system_stream:
212
+ }
157
213
  )
214
+ @gp_to_sp_map = EventSubscriptionPositionQueries.new(
215
+ PgEventstore.connection(current_config)
216
+ ).subscription_positions_from_db(@collection.collection)
158
217
 
159
218
  if request.xhr?
160
219
  content_type 'application/json'
161
220
  halt 200, {
162
- events: erb(:'home/partials/events', { layout: false }, { events: @collection.collection }),
221
+ events: erb(
222
+ :'home/partials/events',
223
+ { layout: false },
224
+ { events: @collection.collection, gp_to_sp_map: @gp_to_sp_map }
225
+ ),
163
226
  total_count: total_count(@collection.total_count),
164
227
  pagination: erb(:'home/partials/pagination_links', { layout: false }, { collection: @collection }),
165
228
  }.to_json
@@ -168,6 +231,16 @@ module PgEventstore
168
231
  end
169
232
  end
170
233
 
234
+ get '/streams' do
235
+ @collection = Paginator::StreamsCollection.new(
236
+ current_config,
237
+ starting_id: params[:starting_id]&.to_i,
238
+ per_page: Paginator::StreamsCollection::PER_PAGE[params[:per_page]],
239
+ order: Paginator::StreamsCollection::SQL_DIRECTIONS[params[:order]]
240
+ )
241
+ erb :'streams/index'
242
+ end
243
+
171
244
  get '/subscriptions' do
172
245
  @set_collection = Subscriptions::SetCollection.new(connection)
173
246
  @current_set = params[:set_name] || @set_collection.names.first
@@ -247,6 +320,17 @@ module PgEventstore
247
320
  paginated_json_response(collection)
248
321
  end
249
322
 
323
+ get '/markers_filtering', provides: :json do
324
+ collection = Paginator::MarkersCollection.new(
325
+ current_config,
326
+ starting_id: unescape_empty_string(params[:starting_id]),
327
+ per_page: Paginator::MarkersCollection::PER_PAGE,
328
+ order: :asc,
329
+ options: { query: params[:term] }
330
+ )
331
+ paginated_json_response(collection)
332
+ end
333
+
250
334
  post '/subscription_cmd/:set_id/:id/:cmd' do
251
335
  validate_subscription_cmd(params[:cmd])
252
336
  cmd_class = SubscriptionRunnerCommands.command_class(params[:cmd])
@@ -279,14 +363,14 @@ module PgEventstore
279
363
  end
280
364
 
281
365
  post '/delete_subscription/:id' do
282
- SubscriptionQueries.new(connection).delete(Integer(params[:id]))
366
+ SubscriptionQueries.new(connection, QueryStrategy::Foreground.new(connection)).delete(Integer(params[:id]))
283
367
 
284
368
  redirect redirect_back_url(fallback_url: url('/subscriptions'))
285
369
  end
286
370
 
287
371
  post '/delete_all_subscriptions' do
288
372
  params[:ids].each do |id|
289
- SubscriptionQueries.new(connection).delete(Integer(id))
373
+ SubscriptionQueries.new(connection, QueryStrategy::Foreground.new(connection)).delete(Integer(id))
290
374
  end
291
375
 
292
376
  redirect redirect_back_url(fallback_url: url('/subscriptions'))
@@ -4,6 +4,9 @@ module PgEventstore
4
4
  module Web
5
5
  module Paginator
6
6
  class BaseCollection
7
+ # This is the minimum amount of symbols requeried to perform search using trigrams gin index
8
+ MIN_QUERY_SIZE_FOR_ADVANCE_SEARCH = 3
9
+
7
10
  # @!attribute config_name
8
11
  # @return [Symbol]
9
12
  attr_reader :config_name
@@ -30,7 +33,8 @@ module PgEventstore
30
33
  @starting_id = starting_id
31
34
  @per_page = per_page
32
35
  @order = order
33
- @options = options
36
+ @options =
37
+ options.is_a?(Sinatra::IndifferentHash) ? Utils.deep_transform_keys(Hash[options], &:to_sym) : options
34
38
  end
35
39
 
36
40
  # @return [Array]
@@ -12,9 +12,9 @@ module PgEventstore
12
12
  @collection ||=
13
13
  begin
14
14
  sql_builder = SQLBuilder.new.select('event_type').from('partitions')
15
- sql_builder.where('context is not null and stream_name is not null')
15
+ sql_builder.where('event_type is not null')
16
16
  sql_builder.where("event_type #{direction_operator} ?", starting_id) if starting_id
17
- sql_builder.where('event_type ilike ?', "%#{options[:query]}%")
17
+ compute_event_type_filter(sql_builder)
18
18
  sql_builder.group('event_type').order("event_type #{order}").limit(per_page)
19
19
  connection.with do |conn|
20
20
  conn.exec_params(*sql_builder.to_exec_params)
@@ -28,9 +28,8 @@ module PgEventstore
28
28
 
29
29
  starting_id = collection.first['event_type']
30
30
  sql_builder = SQLBuilder.new.select('event_type').from('partitions')
31
- sql_builder.where('context is not null and stream_name is not null')
32
31
  sql_builder.where("event_type #{direction_operator} ?", starting_id)
33
- sql_builder.where('event_type ilike ?', "%#{options[:query]}%")
32
+ compute_event_type_filter(sql_builder)
34
33
  sql_builder.group('event_type').order("event_type #{order}").limit(1).offset(per_page)
35
34
 
36
35
  connection.with do |conn|
@@ -44,6 +43,17 @@ module PgEventstore
44
43
  def direction_operator
45
44
  order == :asc ? '>=' : '<='
46
45
  end
46
+
47
+ # @param builder [PgEventstore::SQLBuilder]
48
+ # @return [void]
49
+ def compute_event_type_filter(builder)
50
+ query = options[:query].to_s
51
+ if query.size < MIN_QUERY_SIZE_FOR_ADVANCE_SEARCH
52
+ builder.where('event_type like ?', "#{query}%")
53
+ else
54
+ builder.where('event_type ilike ?', "%#{query}%")
55
+ end
56
+ end
47
57
  end
48
58
  end
49
59
  end
@@ -19,57 +19,46 @@ module PgEventstore
19
19
  # count instead because of the potential performance degradation.
20
20
  MAX_NUMBER_TO_COUNT = 10_000
21
21
 
22
- # @param config_name [Symbol]
23
- # @param starting_id [String, Integer, nil]
24
- # @param per_page [Integer]
25
- # @param order [Symbol] :asc or :desc
26
- # @param options [Hash] additional options to filter the collection
27
- # @param system_stream [String, nil] a name of system stream
28
- def initialize(config_name, starting_id:, per_page:, order:, options: {}, system_stream: nil)
29
- super(config_name, starting_id:, per_page:, order:, options:)
30
- @stream = system_stream ? PgEventstore::Stream.system_stream(system_stream) : PgEventstore::Stream.all_stream
31
- end
32
-
33
22
  # @return [Array<PgEventstore::Event>]
34
23
  def collection
35
- @collection ||= PgEventstore.client(config_name).read(
36
- @stream,
37
- options: options.merge(from_position: starting_id, max_count: per_page, direction: order)
38
- )
24
+ _collection[0...per_page]
39
25
  end
40
26
 
41
27
  # @return [Integer, nil]
42
28
  def next_page_starting_id
43
29
  return unless collection.size == per_page
44
30
 
45
- from_position = event_global_position(collection.first)
46
- sql_builder = QueryBuilders::EventsFiltering.events_filtering(
47
- @stream,
48
- options.merge(from_position:, max_count: 1, direction: order)
49
- ).to_sql_builder.unselect.select('global_position').offset(per_page)
50
- global_position(sql_builder)
31
+ event_global_position(_collection[per_page])
51
32
  end
52
33
 
53
34
  # @return [Integer, nil]
54
35
  def prev_page_starting_id
55
- from_position = event_global_position(collection.first) || starting_id
56
- sql_builder = QueryBuilders::EventsFiltering.events_filtering(
57
- @stream,
58
- options.merge(from_position:, max_count: per_page, direction: order == :asc ? :desc : :asc)
59
- ).to_sql_builder.unselect.select('global_position').offset(1)
60
- sql, params = sql_builder.to_exec_params
61
- sql = "SELECT * FROM (#{sql}) #{Event::PRIMARY_TABLE_NAME} ORDER BY global_position #{order} LIMIT 1"
62
- connection.with do |conn|
63
- conn.exec_params(sql, params)
64
- end.to_a.dig(0, 'global_position')
36
+ starting_position = event_global_position(_collection.first) || starting_id
37
+ return unless starting_position
38
+
39
+ order = self.order == :asc ? :desc : :asc
40
+ starting_position = self.order == :asc ? starting_position - 1 : starting_position + 1
41
+
42
+ # Not ideal from the perspective of the amount of records we load here, but this is the only way to read from
43
+ # the db in the correct way. Direct usage of query builders may result in heavy, unoptimized queries.
44
+ options = self.options.merge(
45
+ from_position: starting_position, max_count: per_page, direction: order, resolve_link_tos: false
46
+ )
47
+ PgEventstore.client(config_name).read(
48
+ PgEventstore::Stream.all_stream,
49
+ options:,
50
+ middlewares: []
51
+ ).last&.global_position
65
52
  end
66
53
 
67
54
  # @return [Integer]
68
55
  def total_count
69
56
  @total_count ||=
70
57
  begin
71
- sql_builder = QueryBuilders::EventsFiltering.events_filtering(@stream, options).to_sql_builder
72
- sql_builder.remove_limit.remove_group.remove_order
58
+ filters_collection = QueryBuilders::Filters::Collection.from_options(options)
59
+ sql_builder = QueryBuilders::IndexBasedEventsFiltering.sql_builder_for_estimate_count(
60
+ filters_collection.collection
61
+ )
73
62
  count = estimate_count(sql_builder)
74
63
  return count if count > MAX_NUMBER_TO_COUNT
75
64
 
@@ -79,6 +68,13 @@ module PgEventstore
79
68
 
80
69
  private
81
70
 
71
+ def _collection
72
+ @_collection ||= PgEventstore.client(config_name).read(
73
+ PgEventstore::Stream.all_stream,
74
+ options: options.merge(from_position: starting_id, max_count: per_page + 1, direction: order)
75
+ )
76
+ end
77
+
82
78
  # @param event [PgEventstore::Event, nil]
83
79
  # @return [Integer, nil]
84
80
  def event_global_position(event)
@@ -80,24 +80,31 @@ module PgEventstore
80
80
  parts.join(delimiter)
81
81
  end
82
82
 
83
- # @param event [PgEventstore::Event]
83
+ # @param stream [PgEventstore::Stream]
84
84
  # @return [String]
85
- def stream_path(event)
85
+ def stream_path(stream)
86
86
  build_path(
87
87
  {
88
88
  filter: {
89
89
  streams: [
90
90
  {
91
- context: escape_empty_string(event.stream.context),
92
- stream_name: escape_empty_string(event.stream.stream_name),
93
- stream_id: escape_empty_string(event.stream.stream_id),
91
+ context: escape_empty_string(stream.context),
92
+ stream_name: escape_empty_string(stream.stream_name),
93
+ stream_id: escape_empty_string(stream.stream_id),
94
94
  },
95
95
  ],
96
96
  },
97
- }
97
+ },
98
+ base_url: '/'
98
99
  )
99
100
  end
100
101
 
102
+ # @param marker [String]
103
+ # @return [String]
104
+ def event_marker_path(marker)
105
+ build_path({ filter: { markers: [escape_empty_string(marker)] } }, base_url: '/')
106
+ end
107
+
101
108
  # @param str [String]
102
109
  # @return [String]
103
110
  def empty_characters_fallback(str)
@@ -118,11 +125,11 @@ module PgEventstore
118
125
 
119
126
  # @param params [Hash, Array]
120
127
  # @return [String]
121
- def build_path(params)
128
+ def build_path(params, base_url: request.path)
122
129
  encoded_params = Rack::Utils.build_nested_query(params)
123
- return request.path if encoded_params.empty?
130
+ return base_url if encoded_params.empty?
124
131
 
125
- "#{request.path}?#{encoded_params}"
132
+ "#{base_url}?#{encoded_params}"
126
133
  end
127
134
  end
128
135
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Web
5
+ module Paginator
6
+ class MarkersCollection < BaseCollection
7
+ # @return [Integer]
8
+ PER_PAGE = 10
9
+
10
+ # @return [Array<Hash<String => String>>]
11
+ def collection
12
+ @collection ||=
13
+ begin
14
+ sql_builder = SQLBuilder.new.select('name').from('event_markers')
15
+ sql_builder.where("name #{direction_operator} ?", starting_id) if starting_id
16
+ sql_builder.where('name like ?', "#{options[:query]}%")
17
+ sql_builder.order("name #{order}").limit(per_page)
18
+ connection.with do |conn|
19
+ conn.exec_params(*sql_builder.to_exec_params)
20
+ end.to_a
21
+ end
22
+ end
23
+
24
+ # @return [String, nil]
25
+ def next_page_starting_id
26
+ return unless collection.size == per_page
27
+
28
+ starting_id = collection.first['name']
29
+ sql_builder = SQLBuilder.new.select('name').from('event_markers')
30
+ sql_builder.where("name #{direction_operator} ?", starting_id)
31
+ sql_builder.where('name like ?', "#{options[:query]}%")
32
+ sql_builder.order("name #{order}").limit(1).offset(per_page)
33
+
34
+ connection.with do |conn|
35
+ conn.exec_params(*sql_builder.to_exec_params)
36
+ end.to_a.dig(0, 'name')
37
+ end
38
+
39
+ private
40
+
41
+ # @return [String]
42
+ def direction_operator
43
+ order == :asc ? '>=' : '<='
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -14,7 +14,7 @@ module PgEventstore
14
14
  sql_builder = SQLBuilder.new.select('context').from('partitions')
15
15
  sql_builder.where('stream_name is null and event_type is null')
16
16
  sql_builder.where("context #{direction_operator} ?", starting_id) if starting_id
17
- sql_builder.where('context ilike ?', "%#{options[:query]}%")
17
+ compute_context_filter(sql_builder)
18
18
  sql_builder.limit(per_page).order("context #{order}")
19
19
  connection.with do |conn|
20
20
  conn.exec_params(*sql_builder.to_exec_params)
@@ -30,7 +30,7 @@ module PgEventstore
30
30
  sql_builder = SQLBuilder.new.select('context').from('partitions')
31
31
  sql_builder.where('stream_name is null and event_type is null')
32
32
  sql_builder.where("context #{direction_operator} ?", starting_id)
33
- sql_builder.where('context ilike ?', "%#{options[:query]}%")
33
+ compute_context_filter(sql_builder)
34
34
  sql_builder.limit(1).offset(per_page).order("context #{order}")
35
35
 
36
36
  connection.with do |conn|
@@ -44,6 +44,17 @@ module PgEventstore
44
44
  def direction_operator
45
45
  order == :asc ? '>=' : '<='
46
46
  end
47
+
48
+ # @param builder [PgEventstore::SQLBuilder]
49
+ # @return [void]
50
+ def compute_context_filter(builder)
51
+ query = options[:query].to_s
52
+ if query.size < MIN_QUERY_SIZE_FOR_ADVANCE_SEARCH
53
+ builder.where('context like ?', "#{query}%")
54
+ else
55
+ builder.where('context ilike ?', "%#{query}%")
56
+ end
57
+ end
47
58
  end
48
59
  end
49
60
  end
@@ -11,11 +11,9 @@ module PgEventstore
11
11
  def collection
12
12
  @collection ||=
13
13
  begin
14
- sql_builder = SQLBuilder.new.select('stream_id').from(Event::PRIMARY_TABLE_NAME)
15
- sql_builder.where('context = ? and stream_name = ?', options[:context], options[:stream_name])
16
- sql_builder.where('stream_id like ?', "#{options[:query]}%")
17
- sql_builder.where("stream_id #{direction_operator} ?", starting_id) if starting_id
18
- sql_builder.group('stream_id').limit(per_page).order("stream_id #{order}")
14
+ sql_builder = streams_index_sql_builder(from_id: starting_id)
15
+ sql_builder.limit(per_page)
16
+
19
17
  connection.with do |conn|
20
18
  conn.exec_params(*sql_builder.to_exec_params)
21
19
  end.to_a
@@ -26,12 +24,8 @@ module PgEventstore
26
24
  def next_page_starting_id
27
25
  return unless collection.size == per_page
28
26
 
29
- starting_id = collection.first['stream_id']
30
- sql_builder = SQLBuilder.new.select('stream_id').from(Event::PRIMARY_TABLE_NAME)
31
- sql_builder.where("stream_id #{direction_operator} ?", starting_id)
32
- sql_builder.where('stream_id like ?', "#{options[:query]}%")
33
- sql_builder.where('context = ? and stream_name = ?', options[:context], options[:stream_name])
34
- sql_builder.group('stream_id').limit(1).offset(per_page).order("stream_id #{order}")
27
+ sql_builder = streams_index_sql_builder(from_id: collection.first['stream_id'])
28
+ sql_builder.limit(1).offset(per_page)
35
29
 
36
30
  connection.with do |conn|
37
31
  conn.exec_params(*sql_builder.to_exec_params)
@@ -40,6 +34,22 @@ module PgEventstore
40
34
 
41
35
  private
42
36
 
37
+ def partitions_sql_builder
38
+ filters = QueryBuilders::Filters::Collection.from_options(
39
+ { filter: { streams: [{ context: options[:context], stream_name: options[:stream_name] }] } }
40
+ )
41
+ QueryBuilders::PartitionsFiltering.assemble_sql_builder(filters, scope: :stream_name).unselect.select('id')
42
+ end
43
+
44
+ def streams_index_sql_builder(from_id: nil)
45
+ sql_builder = SQLBuilder.new.select('stream_id')
46
+ sql_builder.from(QueryBuilders::StreamsGlobalIndexFiltering::PRIMARY_TABLE_NAME)
47
+ sql_builder.where('partition_id = ?', partitions_sql_builder)
48
+ sql_builder.where('stream_id like ?', "#{options[:query]}%")
49
+ sql_builder.where("stream_id #{direction_operator} ?", from_id) if from_id
50
+ sql_builder.order("stream_id #{order}")
51
+ end
52
+
43
53
  # @return [String]
44
54
  def direction_operator
45
55
  order == :asc ? '>=' : '<='
@@ -12,9 +12,9 @@ module PgEventstore
12
12
  @collection ||=
13
13
  begin
14
14
  sql_builder = SQLBuilder.new.select('stream_name').from('partitions')
15
- sql_builder.where('event_type is null and context = ?', options[:context])
16
- sql_builder.where('stream_name ilike ?', "%#{options[:query]}%")
15
+ sql_builder.where('event_type is null and stream_name is not null and context = ?', options[:context])
17
16
  sql_builder.where("stream_name #{direction_operator} ?", starting_id) if starting_id
17
+ compute_stream_name_filter(sql_builder)
18
18
  sql_builder.limit(per_page).order("stream_name #{order}")
19
19
  connection.with do |conn|
20
20
  conn.exec_params(*sql_builder.to_exec_params)
@@ -29,10 +29,9 @@ module PgEventstore
29
29
  starting_id = collection.first['stream_name']
30
30
  sql_builder = SQLBuilder.new.select('stream_name').from('partitions')
31
31
  sql_builder.where("stream_name #{direction_operator} ?", starting_id)
32
- sql_builder.where('stream_name ilike ?', "%#{options[:query]}%")
32
+ compute_stream_name_filter(sql_builder)
33
33
  sql_builder.where('event_type is null and context = ?', options[:context])
34
34
  sql_builder.limit(1).offset(per_page).order("stream_name #{order}")
35
-
36
35
  connection.with do |conn|
37
36
  conn.exec_params(*sql_builder.to_exec_params)
38
37
  end.to_a.dig(0, 'stream_name')
@@ -44,6 +43,21 @@ module PgEventstore
44
43
  def direction_operator
45
44
  order == :asc ? '>=' : '<='
46
45
  end
46
+
47
+ # @param builder [PgEventstore::SQLBuilder]
48
+ # @return [void]
49
+ def compute_stream_name_filter(builder)
50
+ query = options[:query].to_s
51
+ if query.size < MIN_QUERY_SIZE_FOR_ADVANCE_SEARCH
52
+ builder.where('stream_name like ?', "#{query}%")
53
+ else
54
+ builder.where(
55
+ 'context || ? || stream_name ilike ?',
56
+ PgEventstore::Event::SYSTEM_SYMBOL,
57
+ "#{options[:context]}#{PgEventstore::Event::SYSTEM_SYMBOL}%#{query}%"
58
+ )
59
+ end
60
+ end
47
61
  end
48
62
  end
49
63
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Web
5
+ module Paginator
6
+ class StreamsCollection < BaseCollection
7
+ # @return [Hash<String => Symbol>] SQL directions, string-to-symbol mapping
8
+ SQL_DIRECTIONS = {
9
+ 'asc' => :asc,
10
+ 'desc' => :desc,
11
+ }.tap do |directions|
12
+ directions.default = :desc
13
+ end.freeze
14
+ # @return [Hash<String => Integer>] per page limits, string to Integer mapping
15
+ PER_PAGE = %w[10 20 50 100 1000].to_h { [_1, _1.to_i] }.tap do |per_page|
16
+ per_page.default = 10
17
+ end.freeze
18
+ # @return [Integer] max number of events after which we don't perform the exact count and keep the estimate
19
+ # count instead because of the potential performance degradation.
20
+ MAX_NUMBER_TO_COUNT = 10_000
21
+
22
+ # @return [Array<PgEventstore::Stream>]
23
+ def collection
24
+ @collection ||= PgEventstore.client(config_name).read_streams(
25
+ options: { from_position: starting_id, max_count: per_page, direction: order }
26
+ )
27
+ end
28
+
29
+ # @return [Integer, nil]
30
+ def next_page_starting_id
31
+ return unless collection.size == per_page
32
+
33
+ from_position = collection.first&.starting_position
34
+ sql_builder = QueryBuilders::StreamsGlobalIndexFiltering.sql_builder_for_basic_pagination(
35
+ from_position:, max_count: 1, direction: order
36
+ ).unselect.select('starting_position').offset(per_page)
37
+ starting_position(sql_builder)
38
+ end
39
+
40
+ # @return [Integer, nil]
41
+ def prev_page_starting_id
42
+ from_position = collection.first&.starting_position || starting_id
43
+ sql_builder = QueryBuilders::StreamsGlobalIndexFiltering.sql_builder_for_basic_pagination(
44
+ from_position:, max_count: per_page, direction: order == :asc ? :desc : :asc
45
+ ).unselect.select('starting_position').offset(1)
46
+ sql_builder = SQLBuilder.new.select('starting_position').from(sql_builder)
47
+ sql_builder.order("starting_position #{order}").limit(1)
48
+ starting_position(sql_builder)
49
+ end
50
+
51
+ # @return [Integer]
52
+ def total_count
53
+ @total_count ||=
54
+ begin
55
+ sql_builder = QueryBuilders::StreamsGlobalIndexFiltering.sql_builder_for_basic_pagination({})
56
+ sql_builder.remove_limit.remove_group.remove_order
57
+ count = estimate_count(sql_builder)
58
+ return count if count > MAX_NUMBER_TO_COUNT
59
+
60
+ regular_count(sql_builder)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ # @param sql_builder [PgEventstore::SQLBuilder]
67
+ # @return [Integer]
68
+ def estimate_count(sql_builder)
69
+ sql, params = sql_builder.to_exec_params
70
+ connection.with do |conn|
71
+ conn.exec_params("EXPLAIN #{sql}", params)
72
+ end.to_a.first['QUERY PLAN'].match(/rows=(\d+)/)[1].to_i
73
+ end
74
+
75
+ # @param sql_builder [PgEventstore::SQLBuilder]
76
+ # @return [Integer]
77
+ def regular_count(sql_builder)
78
+ sql_builder.unselect.select('count(*) as count_all')
79
+
80
+ connection.with do |conn|
81
+ conn.exec_params(*sql_builder.to_exec_params)
82
+ end.to_a.first['count_all']
83
+ end
84
+
85
+ # @param sql_builder [PgEventstore::SQLBuilder]
86
+ # @return [Integer, nil]
87
+ def starting_position(sql_builder)
88
+ connection.with do |conn|
89
+ conn.exec_params(*sql_builder.to_exec_params)
90
+ end.to_a.dig(0, 'starting_position')
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end