dbdoc_engine 0.1.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 (198) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +331 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/builds/dbdoc_engine/application.css +5 -0
  5. data/app/assets/images/dbdoc_engine/arrowdown.svg +3 -0
  6. data/app/assets/images/dbdoc_engine/arrowhorizontal.svg +3 -0
  7. data/app/assets/images/dbdoc_engine/arrowleft.svg +3 -0
  8. data/app/assets/images/dbdoc_engine/changelog.svg +3 -0
  9. data/app/assets/images/dbdoc_engine/column_stats_dbdocs.svg +23 -0
  10. data/app/assets/images/dbdoc_engine/diagram.svg +3 -0
  11. data/app/assets/images/dbdoc_engine/double_arrow.svg +4 -0
  12. data/app/assets/images/dbdoc_engine/group_bu.svg +3 -0
  13. data/app/assets/images/dbdoc_engine/japan_circle.png +0 -0
  14. data/app/assets/images/dbdoc_engine/log_in_image.png +0 -0
  15. data/app/assets/images/dbdoc_engine/logo.svg +12 -0
  16. data/app/assets/images/dbdoc_engine/orange_changelog.svg +3 -0
  17. data/app/assets/images/dbdoc_engine/orange_fields.svg +23 -0
  18. data/app/assets/images/dbdoc_engine/orange_logo.svg +12 -0
  19. data/app/assets/images/dbdoc_engine/orange_table.svg +21 -0
  20. data/app/assets/images/dbdoc_engine/orange_updates.svg +43 -0
  21. data/app/assets/images/dbdoc_engine/orange_wiki.svg +3 -0
  22. data/app/assets/images/dbdoc_engine/search.svg +3 -0
  23. data/app/assets/images/dbdoc_engine/setting.svg +3 -0
  24. data/app/assets/images/dbdoc_engine/table_dbdocs.svg +21 -0
  25. data/app/assets/images/dbdoc_engine/uk_circle_transparent.png +0 -0
  26. data/app/assets/images/dbdoc_engine/update_stats_dbdocs.svg +43 -0
  27. data/app/assets/images/dbdoc_engine/wiki.svg +3 -0
  28. data/app/assets/stylesheets/dbdoc_engine/admin.css +176 -0
  29. data/app/assets/stylesheets/dbdoc_engine/admin_header.css +179 -0
  30. data/app/assets/stylesheets/dbdoc_engine/application.scss +1 -0
  31. data/app/assets/stylesheets/dbdoc_engine/changelog.css +173 -0
  32. data/app/assets/stylesheets/dbdoc_engine/dashboard.css +513 -0
  33. data/app/assets/stylesheets/dbdoc_engine/dbdoc_application.css +117 -0
  34. data/app/assets/stylesheets/dbdoc_engine/ecommerce.css +253 -0
  35. data/app/assets/stylesheets/dbdoc_engine/group_details.css +178 -0
  36. data/app/assets/stylesheets/dbdoc_engine/header.css +212 -0
  37. data/app/assets/stylesheets/dbdoc_engine/loading_spinner.css +127 -0
  38. data/app/assets/stylesheets/dbdoc_engine/login.css +213 -0
  39. data/app/assets/stylesheets/dbdoc_engine/schema_diagram.css +149 -0
  40. data/app/assets/stylesheets/dbdoc_engine/sidebar.css +296 -0
  41. data/app/assets/stylesheets/dbdoc_engine/table_details.css +417 -0
  42. data/app/controllers/dbdoc_engine/admin/base_controller.rb +23 -0
  43. data/app/controllers/dbdoc_engine/admin/dashboard_controller.rb +16 -0
  44. data/app/controllers/dbdoc_engine/admin/data_transfer_controller.rb +63 -0
  45. data/app/controllers/dbdoc_engine/admin/db_design_dynamic_tables_controller.rb +198 -0
  46. data/app/controllers/dbdoc_engine/admin/db_design_table_groups_controller.rb +107 -0
  47. data/app/controllers/dbdoc_engine/application_controller.rb +65 -0
  48. data/app/controllers/dbdoc_engine/concerns/internationalization.rb +57 -0
  49. data/app/controllers/dbdoc_engine/db_doc_sessions_controller.rb +33 -0
  50. data/app/controllers/dbdoc_engine/home_controller.rb +79 -0
  51. data/app/controllers/dbdoc_engine/schema_diagram_controller.rb +293 -0
  52. data/app/helper/dbdoc_engine/application_helper.rb +35 -0
  53. data/app/helpers/dbdoc_engine/application_helper.rb +4 -0
  54. data/app/helpers/dbdoc_engine/changelogs_helper.rb +27 -0
  55. data/app/helpers/dbdoc_engine/column_helper.rb +30 -0
  56. data/app/helpers/dbdoc_engine/db_design_dynamic_tables_helper.rb +15 -0
  57. data/app/helpers/dbdoc_engine/home_helper.rb +75 -0
  58. data/app/javascript/dbdoc_engine/application.js +12 -0
  59. data/app/javascript/dbdoc_engine/controllers/application.js +29 -0
  60. data/app/javascript/dbdoc_engine/controllers/auto_submit_controller.js +17 -0
  61. data/app/javascript/dbdoc_engine/controllers/chart_controller.js +58 -0
  62. data/app/javascript/dbdoc_engine/controllers/column-type_controller.js +149 -0
  63. data/app/javascript/dbdoc_engine/controllers/column_controller.js +362 -0
  64. data/app/javascript/dbdoc_engine/controllers/column_search_controller.js +42 -0
  65. data/app/javascript/dbdoc_engine/controllers/dbdoc_accordion_controller.js +42 -0
  66. data/app/javascript/dbdoc_engine/controllers/ecommerce_controller.js +73 -0
  67. data/app/javascript/dbdoc_engine/controllers/group_details_controller.js +88 -0
  68. data/app/javascript/dbdoc_engine/controllers/import_export_controller.js +200 -0
  69. data/app/javascript/dbdoc_engine/controllers/index.js +9 -0
  70. data/app/javascript/dbdoc_engine/controllers/language_controller.js +100 -0
  71. data/app/javascript/dbdoc_engine/controllers/loading_spinner_controller.js +48 -0
  72. data/app/javascript/dbdoc_engine/controllers/login_controller.js +75 -0
  73. data/app/javascript/dbdoc_engine/controllers/notification_controller.js +15 -0
  74. data/app/javascript/dbdoc_engine/controllers/schema_diagram_controller.js +1129 -0
  75. data/app/javascript/dbdoc_engine/controllers/select2_controller.js +67 -0
  76. data/app/javascript/dbdoc_engine/controllers/sidebar_controller.js +943 -0
  77. data/app/javascript/dbdoc_engine/controllers/table_details_controller.js +245 -0
  78. data/app/javascript/dbdoc_engine/controllers/table_group_validation_controller.js +148 -0
  79. data/app/javascript/dbdoc_engine/controllers/table_validation_controller.js +423 -0
  80. data/app/jobs/dbdoc_engine/application_job.rb +4 -0
  81. data/app/mailers/dbdoc_engine/application_mailer.rb +6 -0
  82. data/app/models/dbdoc_engine/application_record.rb +6 -0
  83. data/app/models/dbdoc_engine/concerns/soft_deletable.rb +30 -0
  84. data/app/models/dbdoc_engine/db_design_changelog.rb +44 -0
  85. data/app/models/dbdoc_engine/db_design_dynamic_column.rb +211 -0
  86. data/app/models/dbdoc_engine/db_design_dynamic_table.rb +124 -0
  87. data/app/models/dbdoc_engine/db_design_table_group.rb +88 -0
  88. data/app/models/dbdoc_engine/user.rb +21 -0
  89. data/app/queries/dbdoc_engine/admin_dashboard_queries.rb +71 -0
  90. data/app/queries/dbdoc_engine/db_design_changelog_queries.rb +68 -0
  91. data/app/queries/dbdoc_engine/db_design_dynamic_column_queries.rb +37 -0
  92. data/app/queries/dbdoc_engine/db_design_dynamic_table_commands.rb +106 -0
  93. data/app/queries/dbdoc_engine/db_design_dynamic_table_queries.rb +194 -0
  94. data/app/queries/dbdoc_engine/db_design_table_group_queries.rb +154 -0
  95. data/app/services/dbdoc_engine/db_design_dynamic_table_export_service.rb +38 -0
  96. data/app/services/dbdoc_engine/db_design_dynamic_table_handler_service.rb +49 -0
  97. data/app/services/dbdoc_engine/db_design_dynamic_tables_service.rb +21 -0
  98. data/app/services/dbdoc_engine/error_handler_service.rb +43 -0
  99. data/app/services/dbdoc_engine/schema_rb_import_service.rb +194 -0
  100. data/app/services/dbdoc_engine/schema_rb_parser_service.rb +339 -0
  101. data/app/services/dbdoc_engine/table_filter_service.rb +35 -0
  102. data/app/services/dbdoc_engine/table_groups_service.rb +199 -0
  103. data/app/services/dbdoc_engine/table_management_service.rb +192 -0
  104. data/app/views/dbdoc_engine/admin/dashboard/_action_badge.html.erb +11 -0
  105. data/app/views/dbdoc_engine/admin/dashboard/_changelog_rows.html.erb +22 -0
  106. data/app/views/dbdoc_engine/admin/dashboard/_changelog_table_headers.html.erb +8 -0
  107. data/app/views/dbdoc_engine/admin/dashboard/_filter_fields.html.erb +43 -0
  108. data/app/views/dbdoc_engine/admin/dashboard/index.html.erb +159 -0
  109. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_column_fields.html.erb +225 -0
  110. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_deleted_table_index.html.erb +110 -0
  111. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_foreign_key_fields.html.erb +51 -0
  112. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_form.html.erb +75 -0
  113. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_recent_activity.html.erb +39 -0
  114. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_table_columns.html.erb +127 -0
  115. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_table_index.html.erb +109 -0
  116. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_table_information.html.erb +99 -0
  117. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/deleted_tables.html.erb +95 -0
  118. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/edit.html.erb +23 -0
  119. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/export_all_to_excel.xlsx.axlsx +240 -0
  120. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/export_to_excel.xlsx.axlsx +135 -0
  121. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/index.html.erb +109 -0
  122. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/new.html.erb +25 -0
  123. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/show_table_info.html.erb +125 -0
  124. data/app/views/dbdoc_engine/admin/db_design_table_groups/_deleted_table_groups_list.html.erb +75 -0
  125. data/app/views/dbdoc_engine/admin/db_design_table_groups/_form.html.erb +88 -0
  126. data/app/views/dbdoc_engine/admin/db_design_table_groups/_table_groups_list.html.erb +82 -0
  127. data/app/views/dbdoc_engine/admin/db_design_table_groups/deleted_groups.html.erb +60 -0
  128. data/app/views/dbdoc_engine/admin/db_design_table_groups/edit.html.erb +25 -0
  129. data/app/views/dbdoc_engine/admin/db_design_table_groups/index.html.erb +85 -0
  130. data/app/views/dbdoc_engine/admin/db_design_table_groups/new.html.erb +26 -0
  131. data/app/views/dbdoc_engine/db_doc_sessions/new.html.erb +59 -0
  132. data/app/views/dbdoc_engine/home/changelog_details.html.erb +80 -0
  133. data/app/views/dbdoc_engine/home/changelogs.html.erb +20 -0
  134. data/app/views/dbdoc_engine/home/group_details.html.erb +94 -0
  135. data/app/views/dbdoc_engine/home/index.html.erb +11 -0
  136. data/app/views/dbdoc_engine/home/partials/_action_badge.html.erb +11 -0
  137. data/app/views/dbdoc_engine/home/partials/_breadcrumb_navigation.html.erb +30 -0
  138. data/app/views/dbdoc_engine/home/partials/_changelog_rows.html.erb +35 -0
  139. data/app/views/dbdoc_engine/home/partials/_changelog_table_headers.html.erb +16 -0
  140. data/app/views/dbdoc_engine/home/partials/_column_headers.html.erb +23 -0
  141. data/app/views/dbdoc_engine/home/partials/_column_row.html.erb +157 -0
  142. data/app/views/dbdoc_engine/home/partials/_filter_form.html.erb +47 -0
  143. data/app/views/dbdoc_engine/home/partials/_group_section.html.erb +84 -0
  144. data/app/views/dbdoc_engine/home/partials/_pagination.html.erb +5 -0
  145. data/app/views/dbdoc_engine/home/partials/_stats_container.html.erb +46 -0
  146. data/app/views/dbdoc_engine/home/partials/_table_groups.html.erb +7 -0
  147. data/app/views/dbdoc_engine/home/partials/_table_information_section.html.erb +50 -0
  148. data/app/views/dbdoc_engine/home/partials/_table_section.html.erb +48 -0
  149. data/app/views/dbdoc_engine/home/table_details.html.erb +9 -0
  150. data/app/views/dbdoc_engine/schema_diagram/index.html.erb +102 -0
  151. data/app/views/dbdoc_engine/shared/_admin_header.html.erb +78 -0
  152. data/app/views/dbdoc_engine/shared/_header.html.erb +94 -0
  153. data/app/views/dbdoc_engine/shared/_js_translations.html.erb +3 -0
  154. data/app/views/dbdoc_engine/shared/_language_button.html.erb +14 -0
  155. data/app/views/dbdoc_engine/shared/_sidebar.html.erb +128 -0
  156. data/app/views/kaminari/dbdoc_engine/_first_page.html.erb +3 -0
  157. data/app/views/kaminari/dbdoc_engine/_gap.html.erb +3 -0
  158. data/app/views/kaminari/dbdoc_engine/_last_page.html.erb +3 -0
  159. data/app/views/kaminari/dbdoc_engine/_next_page.html.erb +3 -0
  160. data/app/views/kaminari/dbdoc_engine/_page.html.erb +9 -0
  161. data/app/views/kaminari/dbdoc_engine/_paginator.html.erb +17 -0
  162. data/app/views/kaminari/dbdoc_engine/_prev_page.html.erb +3 -0
  163. data/app/views/layouts/dbdoc_engine/application.html.erb +107 -0
  164. data/app/views/layouts/dbdoc_engine/header.html.erb +108 -0
  165. data/config/importmap.rb +11 -0
  166. data/config/locales/en.yml +307 -0
  167. data/config/locales/ja.yml +306 -0
  168. data/config/routes.rb +73 -0
  169. data/db/migrate/rails7/20250227060610_create_db_design_table_groups.rb +15 -0
  170. data/db/migrate/rails7/20250227094626_create_db_design_dynamic_tables.rb +19 -0
  171. data/db/migrate/rails7/20250228022732_create_db_design_dynamic_columns.rb +34 -0
  172. data/db/migrate/rails7/20250401051453_create_db_design_changelogs.rb +26 -0
  173. data/db/migrate/rails7/20250411040822_create_users.rb +14 -0
  174. data/db/migrate/rails7/20250421080851_add_missing_indexes_to_dbdoc_tables.rb +23 -0
  175. data/db/migrate/rails8/20250227060610_create_db_design_table_groups.rb +15 -0
  176. data/db/migrate/rails8/20250227094626_create_db_design_dynamic_tables.rb +19 -0
  177. data/db/migrate/rails8/20250228022732_create_db_design_dynamic_columns.rb +34 -0
  178. data/db/migrate/rails8/20250401051453_create_db_design_changelogs.rb +26 -0
  179. data/db/migrate/rails8/20250411040822_create_users.rb +14 -0
  180. data/db/migrate/rails8/20250421080851_add_missing_indexes_to_dbdoc_tables.rb +23 -0
  181. data/db/seeds.rb +28 -0
  182. data/lib/dbdoc_engine/engine.rb +57 -0
  183. data/lib/dbdoc_engine/version.rb +3 -0
  184. data/lib/dbdoc_engine.rb +9 -0
  185. data/lib/generators/dbdoc_engine/install/install_generator.rb +245 -0
  186. data/lib/generators/dbdoc_engine/uninstall/uninstall_generator.rb +196 -0
  187. data/lib/tasks/dbdoc_engine_tasks.rake +44 -0
  188. data/public/dbdoc_engine_assets/images/camel_chess_head.png +0 -0
  189. data/public/dbdoc_engine_assets/images/dblogo.svg +4 -0
  190. data/public/dbdoc_engine_assets/images/japan_circle.png +0 -0
  191. data/public/dbdoc_engine_assets/images/king_chess_head.png +0 -0
  192. data/public/dbdoc_engine_assets/images/login-bg.svg +44 -0
  193. data/public/dbdoc_engine_assets/images/logo.png +0 -0
  194. data/public/dbdoc_engine_assets/images/logo.svg +12 -0
  195. data/public/dbdoc_engine_assets/images/queen_chess_head.png +0 -0
  196. data/public/dbdoc_engine_assets/images/soldier_chess_headd.png +0 -0
  197. data/public/dbdoc_engine_assets/images/uk_circle_transparent.png +0 -0
  198. metadata +415 -0
@@ -0,0 +1,194 @@
1
+ module DbdocEngine
2
+ class SchemaRbImportService
3
+ DEFAULT_GROUP_NAME = "Imported Schema".freeze
4
+ DEFAULT_GROUP_COLOR = "#6c757d".freeze
5
+ SKIP_TABLES = %w[
6
+ ar_internal_metadata
7
+ schema_migrations
8
+ active_storage_blobs
9
+ active_storage_attachments
10
+ active_storage_variant_records
11
+ action_text_rich_texts
12
+ solid_queue_blocked_executions
13
+ solid_queue_claimed_executions
14
+ solid_queue_failed_executions
15
+ solid_queue_jobs
16
+ solid_queue_pauses
17
+ solid_queue_processes
18
+ solid_queue_ready_executions
19
+ solid_queue_recurring_executions
20
+ solid_queue_recurring_tasks
21
+ solid_queue_scheduled_executions
22
+ solid_queue_semaphores
23
+ solid_cache_entries
24
+ solid_cable_messages
25
+ ].freeze
26
+
27
+ def initialize(schema_content, imported_by:)
28
+ @schema_content = schema_content
29
+ @imported_by = imported_by
30
+ end
31
+
32
+ def import!
33
+ parsed = SchemaRbParserService.new(@schema_content).parse
34
+ tables_data = parsed[:tables].reject { |t| SKIP_TABLES.include?(t[:table_name]) }
35
+
36
+ raise "No tables found in the schema file." if tables_data.empty?
37
+
38
+ total_columns = 0
39
+
40
+ ActiveRecord::Base.transaction do
41
+ clear_existing_data
42
+
43
+ suppress_changelog_callbacks do
44
+ group = create_default_group
45
+ total_columns = bulk_import_tables_and_columns(tables_data, group)
46
+ end
47
+
48
+ log_import_summary(tables_data.size, total_columns)
49
+ end
50
+
51
+ { status: "ok", tables_count: tables_data.size }
52
+ rescue => e
53
+ { status: "error", error: e.message }
54
+ end
55
+
56
+ private
57
+
58
+ def clear_existing_data
59
+ DbDesignDynamicColumn.unscoped.delete_all
60
+ DbDesignDynamicTable.unscoped.delete_all
61
+ DbDesignTableGroup.unscoped.delete_all
62
+ end
63
+
64
+ def create_default_group
65
+ DbDesignTableGroup.create!(
66
+ group_name: DEFAULT_GROUP_NAME,
67
+ group_color: DEFAULT_GROUP_COLOR,
68
+ created_by: @imported_by
69
+ )
70
+ end
71
+
72
+ def bulk_import_tables_and_columns(tables_data, group)
73
+ now = Time.current
74
+
75
+ table_rows = tables_data.map do |td|
76
+ {
77
+ table_name: td[:table_name],
78
+ physical_table_name: td[:table_name],
79
+ db_design_table_group_id: group.id,
80
+ description: build_table_description(td),
81
+ created_by: @imported_by,
82
+ created_at: now,
83
+ updated_at: now
84
+ }
85
+ end
86
+
87
+ DbDesignDynamicTable.insert_all!(table_rows)
88
+
89
+ inserted_tables = DbDesignDynamicTable.unscoped
90
+ .where(db_design_table_group_id: group.id)
91
+ .pluck(:table_name, :id)
92
+ .to_h
93
+
94
+ column_rows = []
95
+
96
+ tables_data.each do |td|
97
+ table_id = inserted_tables[td[:table_name]]
98
+ next unless table_id
99
+
100
+ indexed_columns = collect_indexed_columns(td[:indexes])
101
+ unique_columns = collect_unique_columns(td[:indexes])
102
+
103
+ (td[:columns] || []).each do |col|
104
+ column_rows << {
105
+ db_design_dynamic_table_id: table_id,
106
+ column_name: col[:column_name],
107
+ physical_column_name: col[:column_name],
108
+ data_type: col[:data_type],
109
+ length: col[:length],
110
+ decimal_precision: col[:decimal_precision],
111
+ is_primary_key: col[:is_primary_key] || false,
112
+ is_foreign_key: col[:is_foreign_key] || false,
113
+ foreign_table_name: col[:foreign_table_name],
114
+ foreign_column_name: col[:foreign_column_name],
115
+ is_candidate_key: col[:is_candidate_key] || false,
116
+ is_unique_key: col[:is_unique_key] || unique_columns.include?(col[:column_name]),
117
+ is_indexed: col[:is_indexed] || indexed_columns.include?(col[:column_name]),
118
+ not_null: col[:not_null] || false,
119
+ default_value: col[:default_value],
120
+ description: col[:description],
121
+ created_by: @imported_by,
122
+ created_at: now,
123
+ updated_at: now
124
+ }
125
+ end
126
+ end
127
+
128
+ DbDesignDynamicColumn.insert_all!(column_rows) if column_rows.any?
129
+
130
+ column_rows.size
131
+ end
132
+
133
+ def collect_indexed_columns(indexes)
134
+ return Set.new if indexes.blank?
135
+
136
+ indexes.each_with_object(Set.new) do |idx, set|
137
+ next unless idx && idx[:columns]
138
+
139
+ idx[:columns].each { |c| set.add(c) } if idx[:columns].size == 1
140
+ end
141
+ end
142
+
143
+ def collect_unique_columns(indexes)
144
+ return Set.new if indexes.blank?
145
+
146
+ indexes.select { |idx| idx && idx[:unique] }.each_with_object(Set.new) do |idx, set|
147
+ idx[:columns].each { |c| set.add(c) } if idx[:columns].size == 1
148
+ end
149
+ end
150
+
151
+ def suppress_changelog_callbacks
152
+ callbacks_to_skip = [
153
+ [DbDesignTableGroup, :create, :after, :log_creation],
154
+ [DbDesignTableGroup, :update, :after, :log_update],
155
+ [DbDesignTableGroup, :destroy, :before, :log_deletion],
156
+ [DbDesignDynamicTable, :create, :after, :log_creation],
157
+ [DbDesignDynamicTable, :update, :after, :log_update],
158
+ [DbDesignDynamicTable, :destroy, :before, :log_deletion],
159
+ [DbDesignDynamicColumn, :create, :after, :log_creation],
160
+ [DbDesignDynamicColumn, :update, :after, :log_update],
161
+ [DbDesignDynamicColumn, :destroy, :before, :log_deletion]
162
+ ]
163
+
164
+ callbacks_to_skip.each { |model, event, timing, method| model.skip_callback(event, timing, method, raise: false) }
165
+ yield
166
+ ensure
167
+ callbacks_to_skip.each { |model, event, timing, method| model.set_callback(event, timing, method) rescue nil }
168
+ end
169
+
170
+ def log_import_summary(tables_count, columns_count)
171
+ DbDesignChangelog.create!(
172
+ change_timestamp: Time.current,
173
+ action_type: DbDesignChangelog::ACTION_CREATE,
174
+ entity_type: DbDesignChangelog::ENTITY_TABLE,
175
+ entity_name: "schema_import",
176
+ description: "Imported #{tables_count} tables with #{columns_count} columns from schema.rb",
177
+ changed_by: @imported_by
178
+ )
179
+ end
180
+
181
+ def build_table_description(table_data)
182
+ parts = []
183
+ col_count = table_data[:columns]&.size || 0
184
+ idx_count = table_data[:indexes]&.size || 0
185
+ fk_count = table_data[:columns]&.count { |c| c[:is_foreign_key] } || 0
186
+
187
+ parts << "#{col_count} columns"
188
+ parts << "#{idx_count} indexes" if idx_count > 0
189
+ parts << "#{fk_count} foreign keys" if fk_count > 0
190
+
191
+ "Imported from schema.rb (#{parts.join(', ')})"
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,339 @@
1
+ module DbdocEngine
2
+ class SchemaRbParserService
3
+ RAILS_TYPE_MAP = {
4
+ "string" => "varchar",
5
+ "text" => "text",
6
+ "integer" => "integer",
7
+ "bigint" => "bigint",
8
+ "float" => "float",
9
+ "decimal" => "decimal",
10
+ "boolean" => "boolean",
11
+ "datetime" => "datetime",
12
+ "date" => "date",
13
+ "time" => "time",
14
+ "binary" => "binary",
15
+ "json" => "json",
16
+ "jsonb" => "jsonb",
17
+ "uuid" => "uuid",
18
+ "inet" => "inet",
19
+ "cidr" => "cidr",
20
+ "macaddr" => "macaddr",
21
+ "hstore" => "hstore",
22
+ "serial" => "serial",
23
+ "tsrange" => "tsrange",
24
+ "daterange" => "daterange",
25
+ "int4range" => "int4range",
26
+ "int8range" => "int8range",
27
+ "numrange" => "numrange",
28
+ "point" => "point",
29
+ "line" => "line",
30
+ "circle" => "circle",
31
+ "polygon" => "polygon",
32
+ "xml" => "xml",
33
+ "money" => "money",
34
+ "virtual" => "virtual"
35
+ }.freeze
36
+
37
+ TIMESTAMP_COLUMNS = %w[created_at updated_at].freeze
38
+
39
+ def initialize(schema_content)
40
+ @schema_content = schema_content
41
+ @tables = []
42
+ @foreign_keys = []
43
+ @standalone_indexes = []
44
+ end
45
+
46
+ def parse
47
+ extract_tables
48
+ extract_standalone_foreign_keys
49
+ extract_standalone_indexes
50
+ apply_foreign_keys_to_columns
51
+ apply_standalone_indexes
52
+
53
+ { tables: @tables, foreign_keys: @foreign_keys }
54
+ end
55
+
56
+ private
57
+
58
+ def extract_tables
59
+ @schema_content.scan(/create_table\s+["'](\w+)["'].*?do\s*\|(\w+)\|(.*?)^\s*end/m).each do |match|
60
+ table_name = match[0]
61
+ block_var = match[1]
62
+ block_body = match[2]
63
+
64
+ columns = []
65
+ indexes = []
66
+
67
+ block_body.each_line do |line|
68
+ line = line.strip
69
+ next if line.empty? || line.start_with?("#")
70
+
71
+ if line.match?(/\A#{Regexp.escape(block_var)}\.index\b/)
72
+ indexes << parse_inline_index(line, block_var)
73
+ elsif line.match?(/\A#{Regexp.escape(block_var)}\.timestamps\b/)
74
+ columns.concat(build_timestamp_columns(line))
75
+ elsif line.match?(/\A#{Regexp.escape(block_var)}\.(references|belongs_to)\b/)
76
+ columns << parse_reference_column(line, block_var)
77
+ elsif line.match?(/\A#{Regexp.escape(block_var)}\.(\w+)\s/)
78
+ col = parse_column(line, block_var)
79
+ columns << col if col
80
+ end
81
+ end
82
+
83
+ @tables << {
84
+ table_name: table_name,
85
+ columns: columns,
86
+ indexes: indexes
87
+ }
88
+ end
89
+ end
90
+
91
+ def parse_column(line, block_var)
92
+ match = line.match(/\A#{Regexp.escape(block_var)}\.(\w+)\s+["'](\w+)["'](.*)/)
93
+ return nil unless match
94
+
95
+ rails_type = match[1]
96
+ column_name = match[2]
97
+ options_str = match[3]
98
+
99
+ return nil unless RAILS_TYPE_MAP.key?(rails_type)
100
+
101
+ options = parse_options(options_str)
102
+
103
+ {
104
+ column_name: column_name,
105
+ data_type: RAILS_TYPE_MAP[rails_type],
106
+ not_null: options[:null] == false,
107
+ default_value: format_default(options[:default]),
108
+ length: options[:limit],
109
+ decimal_precision: options[:precision],
110
+ description: options[:comment],
111
+ is_primary_key: false,
112
+ is_foreign_key: false,
113
+ is_indexed: false,
114
+ is_candidate_key: false,
115
+ is_unique_key: false
116
+ }
117
+ end
118
+
119
+ def parse_reference_column(line, block_var)
120
+ match = line.match(/\A#{Regexp.escape(block_var)}\.(references|belongs_to)\s+[:]?["']?(\w+)["']?(.*)/)
121
+ return nil unless match
122
+
123
+ ref_name = match[2]
124
+ options_str = match[3]
125
+ options = parse_options(options_str)
126
+
127
+ column_name = "#{ref_name}_id"
128
+ foreign_table = options[:to_table] || ref_name.pluralize
129
+
130
+ col = {
131
+ column_name: column_name,
132
+ data_type: "bigint",
133
+ not_null: options[:null] == false,
134
+ default_value: nil,
135
+ length: nil,
136
+ decimal_precision: nil,
137
+ description: options[:comment],
138
+ is_primary_key: false,
139
+ is_foreign_key: !!options[:foreign_key],
140
+ foreign_table_name: options[:foreign_key] ? foreign_table : nil,
141
+ foreign_column_name: options[:foreign_key] ? "id" : nil,
142
+ is_indexed: true,
143
+ is_candidate_key: false,
144
+ is_unique_key: false
145
+ }
146
+
147
+ if options[:foreign_key].is_a?(Hash) && options[:foreign_key][:to_table]
148
+ col[:foreign_table_name] = options[:foreign_key][:to_table].to_s
149
+ end
150
+
151
+ col
152
+ end
153
+
154
+ def build_timestamp_columns(line)
155
+ options = parse_options(line.sub(/\A\w+\.timestamps/, ""))
156
+ is_not_null = options[:null] == false
157
+
158
+ TIMESTAMP_COLUMNS.map do |col_name|
159
+ {
160
+ column_name: col_name,
161
+ data_type: "datetime",
162
+ not_null: is_not_null,
163
+ default_value: nil,
164
+ length: nil,
165
+ decimal_precision: nil,
166
+ description: nil,
167
+ is_primary_key: false,
168
+ is_foreign_key: false,
169
+ is_indexed: false,
170
+ is_candidate_key: false,
171
+ is_unique_key: false
172
+ }
173
+ end
174
+ end
175
+
176
+ def parse_inline_index(line, block_var)
177
+ cols_match = line.match(/\A#{Regexp.escape(block_var)}\.index\s+\[([^\]]+)\](.*)/) ||
178
+ line.match(/\A#{Regexp.escape(block_var)}\.index\s+["'](\w+)["'](.*)/)
179
+
180
+ return nil unless cols_match
181
+
182
+ raw_cols = cols_match[1]
183
+ options_str = cols_match[2]
184
+
185
+ columns = if raw_cols.include?(",")
186
+ raw_cols.scan(/["'](\w+)["']/).flatten
187
+ else
188
+ [raw_cols.delete("'\"").strip]
189
+ end
190
+
191
+ options = parse_options(options_str)
192
+
193
+ {
194
+ columns: columns,
195
+ unique: options[:unique] == true,
196
+ name: options[:name]
197
+ }
198
+ end
199
+
200
+ def extract_standalone_foreign_keys
201
+ @schema_content.scan(/add_foreign_key\s+["'](\w+)["']\s*,\s*["'](\w+)["'](.*)/).each do |match|
202
+ from_table = match[0]
203
+ to_table = match[1]
204
+ options_str = match[2]
205
+ options = parse_options(options_str)
206
+ column = options[:column] || "#{to_table.singularize}_id"
207
+
208
+ @foreign_keys << {
209
+ from_table: from_table,
210
+ to_table: to_table,
211
+ column: column.to_s,
212
+ primary_key: (options[:primary_key] || "id").to_s
213
+ }
214
+ end
215
+ end
216
+
217
+ def extract_standalone_indexes
218
+ @schema_content.scan(/^\s*add_index\s+[:"'](\w+)["']?\s*,\s*(.+)/).each do |match|
219
+ table_name = match[0]
220
+ rest = match[1]
221
+
222
+ cols_match = rest.match(/\[([^\]]+)\](.*)/) || rest.match(/[:"'](\w+)["']?(.*)/)
223
+ next unless cols_match
224
+
225
+ raw_cols = cols_match[1]
226
+ options_str = cols_match[2]
227
+
228
+ columns = if raw_cols.include?(",")
229
+ raw_cols.scan(/["']?(\w+)["']?/).flatten
230
+ else
231
+ [raw_cols.delete(":'\"").strip]
232
+ end
233
+
234
+ options = parse_options(options_str)
235
+
236
+ @standalone_indexes << {
237
+ table_name: table_name,
238
+ columns: columns,
239
+ unique: options[:unique] == true,
240
+ name: options[:name]
241
+ }
242
+ end
243
+ end
244
+
245
+ def apply_foreign_keys_to_columns
246
+ @foreign_keys.each do |fk|
247
+ table = @tables.find { |t| t[:table_name] == fk[:from_table] }
248
+ next unless table
249
+
250
+ col = table[:columns].find { |c| c[:column_name] == fk[:column] }
251
+ next unless col
252
+
253
+ col[:is_foreign_key] = true
254
+ col[:foreign_table_name] = fk[:to_table]
255
+ col[:foreign_column_name] = fk[:primary_key]
256
+ end
257
+ end
258
+
259
+ def apply_standalone_indexes
260
+ @standalone_indexes.each do |idx|
261
+ table = @tables.find { |t| t[:table_name] == idx[:table_name] }
262
+ next unless table
263
+
264
+ if idx[:columns].size == 1
265
+ col = table[:columns].find { |c| c[:column_name] == idx[:columns].first }
266
+ if col
267
+ col[:is_indexed] = true
268
+ col[:is_unique_key] = true if idx[:unique]
269
+ end
270
+ end
271
+
272
+ table[:indexes] << idx
273
+ end
274
+ end
275
+
276
+ def parse_options(options_str)
277
+ return {} if options_str.nil? || options_str.strip.empty?
278
+
279
+ opts = {}
280
+
281
+ opts[:null] = false if options_str.match?(/null:\s*false/)
282
+ opts[:null] = true if options_str.match?(/null:\s*true/)
283
+
284
+ opts[:unique] = true if options_str.match?(/unique:\s*true/)
285
+
286
+ if (m = options_str.match(/default:\s*("([^"]*)"|'([^']*)'|(\S+?)(?:\s*,|\s*$))/))
287
+ val = m[2] || m[3] || m[4]
288
+ opts[:default] = coerce_default(val)
289
+ end
290
+
291
+ if (m = options_str.match(/limit:\s*(\d+)/))
292
+ opts[:limit] = m[1].to_i
293
+ end
294
+
295
+ if (m = options_str.match(/precision:\s*(\d+)/))
296
+ opts[:precision] = m[1].to_i
297
+ end
298
+
299
+ if (m = options_str.match(/comment:\s*["']([^"']+)["']/))
300
+ opts[:comment] = m[1]
301
+ end
302
+
303
+ if (m = options_str.match(/name:\s*["']([^"']+)["']/))
304
+ opts[:name] = m[1]
305
+ end
306
+
307
+ opts[:foreign_key] = true if options_str.match?(/foreign_key:\s*true/)
308
+
309
+ if (m = options_str.match(/foreign_key:\s*\{([^}]+)\}/))
310
+ fk_opts = {}
311
+ fk_opts[:to_table] = m[1].match(/to_table:\s*[:"'](\w+)/)[1] if m[1].match?(/to_table:/)
312
+ opts[:foreign_key] = fk_opts
313
+ end
314
+
315
+ if (m = options_str.match(/column:\s*[:"'](\w+)/))
316
+ opts[:column] = m[1]
317
+ end
318
+
319
+ if (m = options_str.match(/primary_key:\s*[:"'](\w+)/))
320
+ opts[:primary_key] = m[1]
321
+ end
322
+
323
+ opts
324
+ end
325
+
326
+ def coerce_default(val)
327
+ return nil if val.nil?
328
+ return val.to_s if val.is_a?(String)
329
+
330
+ val.to_s
331
+ end
332
+
333
+ def format_default(val)
334
+ return nil if val.nil?
335
+
336
+ val.to_s
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,35 @@
1
+ # Service for filtering table queries based on group and search parameters
2
+ module DbdocEngine
3
+ class TableFilterService
4
+ attr_reader :tables, :params
5
+
6
+ # Initialize with a base tables relation and filtering params
7
+ def initialize(tables, params)
8
+ @tables = tables
9
+ @params = params
10
+ end
11
+
12
+ # Apply all filters and return the filtered tables relation
13
+ def filter
14
+ @tables = apply_group_filter # Filter by group if param present
15
+ @tables = apply_search # Filter by search if param present
16
+ tables # Return the final filtered relation
17
+ end
18
+
19
+ private
20
+
21
+ # Filter tables by group if group_filter param is present
22
+ def apply_group_filter
23
+ return tables unless params[:group_filter].present?
24
+
25
+ DbdocEngine::DbDesignDynamicTableQueries.filter_by_group(tables, params[:group_filter])
26
+ end
27
+
28
+ # Filter tables by search term if search param is present
29
+ def apply_search
30
+ return tables unless params[:search].present?
31
+
32
+ DbdocEngine::DbDesignDynamicTableQueries.search_tables(tables, params[:search])
33
+ end
34
+ end
35
+ end