better_structure_sql 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 (68) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +41 -0
  3. data/LICENSE +21 -0
  4. data/README.md +557 -0
  5. data/app/controllers/better_structure_sql/application_controller.rb +61 -0
  6. data/app/controllers/better_structure_sql/schema_versions_controller.rb +243 -0
  7. data/app/helpers/better_structure_sql/schema_versions_helper.rb +46 -0
  8. data/app/views/better_structure_sql/schema_versions/index.html.erb +110 -0
  9. data/app/views/better_structure_sql/schema_versions/show.html.erb +186 -0
  10. data/app/views/layouts/better_structure_sql/application.html.erb +105 -0
  11. data/config/database.yml +3 -0
  12. data/config/routes.rb +12 -0
  13. data/lib/better_structure_sql/adapters/base_adapter.rb +234 -0
  14. data/lib/better_structure_sql/adapters/mysql_adapter.rb +476 -0
  15. data/lib/better_structure_sql/adapters/mysql_config.rb +32 -0
  16. data/lib/better_structure_sql/adapters/postgresql_adapter.rb +646 -0
  17. data/lib/better_structure_sql/adapters/postgresql_config.rb +25 -0
  18. data/lib/better_structure_sql/adapters/registry.rb +115 -0
  19. data/lib/better_structure_sql/adapters/sqlite_adapter.rb +644 -0
  20. data/lib/better_structure_sql/adapters/sqlite_config.rb +26 -0
  21. data/lib/better_structure_sql/configuration.rb +129 -0
  22. data/lib/better_structure_sql/database_version.rb +46 -0
  23. data/lib/better_structure_sql/dependency_resolver.rb +63 -0
  24. data/lib/better_structure_sql/dumper.rb +544 -0
  25. data/lib/better_structure_sql/engine.rb +28 -0
  26. data/lib/better_structure_sql/file_writer.rb +180 -0
  27. data/lib/better_structure_sql/formatter.rb +70 -0
  28. data/lib/better_structure_sql/generators/base.rb +33 -0
  29. data/lib/better_structure_sql/generators/domain_generator.rb +22 -0
  30. data/lib/better_structure_sql/generators/extension_generator.rb +23 -0
  31. data/lib/better_structure_sql/generators/foreign_key_generator.rb +43 -0
  32. data/lib/better_structure_sql/generators/function_generator.rb +33 -0
  33. data/lib/better_structure_sql/generators/index_generator.rb +50 -0
  34. data/lib/better_structure_sql/generators/materialized_view_generator.rb +31 -0
  35. data/lib/better_structure_sql/generators/pragma_generator.rb +23 -0
  36. data/lib/better_structure_sql/generators/sequence_generator.rb +27 -0
  37. data/lib/better_structure_sql/generators/table_generator.rb +126 -0
  38. data/lib/better_structure_sql/generators/trigger_generator.rb +54 -0
  39. data/lib/better_structure_sql/generators/type_generator.rb +47 -0
  40. data/lib/better_structure_sql/generators/view_generator.rb +27 -0
  41. data/lib/better_structure_sql/introspection/extensions.rb +29 -0
  42. data/lib/better_structure_sql/introspection/foreign_keys.rb +29 -0
  43. data/lib/better_structure_sql/introspection/functions.rb +29 -0
  44. data/lib/better_structure_sql/introspection/indexes.rb +29 -0
  45. data/lib/better_structure_sql/introspection/sequences.rb +29 -0
  46. data/lib/better_structure_sql/introspection/tables.rb +29 -0
  47. data/lib/better_structure_sql/introspection/triggers.rb +29 -0
  48. data/lib/better_structure_sql/introspection/types.rb +37 -0
  49. data/lib/better_structure_sql/introspection/views.rb +41 -0
  50. data/lib/better_structure_sql/introspection.rb +31 -0
  51. data/lib/better_structure_sql/manifest_generator.rb +65 -0
  52. data/lib/better_structure_sql/migration_patch.rb +196 -0
  53. data/lib/better_structure_sql/pg_version.rb +44 -0
  54. data/lib/better_structure_sql/railtie.rb +124 -0
  55. data/lib/better_structure_sql/schema_loader.rb +168 -0
  56. data/lib/better_structure_sql/schema_version.rb +86 -0
  57. data/lib/better_structure_sql/schema_versions.rb +213 -0
  58. data/lib/better_structure_sql/version.rb +5 -0
  59. data/lib/better_structure_sql/zip_generator.rb +81 -0
  60. data/lib/better_structure_sql.rb +81 -0
  61. data/lib/generators/better_structure_sql/install_generator.rb +44 -0
  62. data/lib/generators/better_structure_sql/migration_generator.rb +34 -0
  63. data/lib/generators/better_structure_sql/templates/README +49 -0
  64. data/lib/generators/better_structure_sql/templates/add_metadata_migration.rb.erb +25 -0
  65. data/lib/generators/better_structure_sql/templates/better_structure_sql.rb +46 -0
  66. data/lib/generators/better_structure_sql/templates/migration.rb.erb +26 -0
  67. data/lib/tasks/better_structure_sql.rake +190 -0
  68. metadata +299 -0
@@ -0,0 +1,105 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Schema Versions - BetterStructureSql</title>
7
+
8
+ <!-- Bootstrap 5.3 CSS from CDN -->
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
10
+
11
+ <!-- Bootstrap Icons from CDN -->
12
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css">
13
+
14
+ <style>
15
+ .code-block {
16
+ background: #f8f9fa;
17
+ border: 1px solid #dee2e6;
18
+ border-radius: 0.375rem;
19
+ padding: 1rem;
20
+ overflow-x: auto;
21
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
22
+ font-size: 0.875rem;
23
+ line-height: 1.5;
24
+ }
25
+
26
+ .metadata-card {
27
+ background: #f8f9fa;
28
+ border-left: 4px solid #0d6efd;
29
+ }
30
+
31
+ .navbar-brand {
32
+ font-weight: 600;
33
+ }
34
+
35
+ .empty-state {
36
+ padding: 4rem 2rem;
37
+ text-align: center;
38
+ color: #6c757d;
39
+ }
40
+
41
+ .badge-format {
42
+ font-weight: 500;
43
+ }
44
+ </style>
45
+ </head>
46
+ <body>
47
+ <!-- Navigation -->
48
+ <nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4">
49
+ <div class="container-fluid">
50
+ <a class="navbar-brand" href="<%= better_structure_sql.schema_versions_path %>">
51
+ <i class="bi bi-database"></i>
52
+ BetterStructureSql
53
+ </a>
54
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
55
+ <span class="navbar-toggler-icon"></span>
56
+ </button>
57
+ <div class="collapse navbar-collapse" id="navbarNav">
58
+ <ul class="navbar-nav ms-auto">
59
+ <li class="nav-item">
60
+ <a class="nav-link" href="<%= better_structure_sql.schema_versions_path %>">
61
+ <i class="bi bi-list-ul"></i>
62
+ All Versions
63
+ </a>
64
+ </li>
65
+ </ul>
66
+ </div>
67
+ </div>
68
+ </nav>
69
+
70
+ <!-- Flash Messages -->
71
+ <div class="container-fluid">
72
+ <% if flash[:notice] %>
73
+ <div class="alert alert-success alert-dismissible fade show" role="alert">
74
+ <%= flash[:notice] %>
75
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
76
+ </div>
77
+ <% end %>
78
+
79
+ <% if flash[:alert] %>
80
+ <div class="alert alert-danger alert-dismissible fade show" role="alert">
81
+ <%= flash[:alert] %>
82
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
83
+ </div>
84
+ <% end %>
85
+ </div>
86
+
87
+ <!-- Main Content -->
88
+ <main class="container-fluid">
89
+ <%= yield %>
90
+ </main>
91
+
92
+ <!-- Footer -->
93
+ <footer class="mt-5 py-4 bg-light border-top">
94
+ <div class="container-fluid text-center text-muted">
95
+ <small>
96
+ BetterStructureSql Engine &middot;
97
+ Clean PostgreSQL schema dumps for Rails
98
+ </small>
99
+ </div>
100
+ </footer>
101
+
102
+ <!-- Bootstrap JS Bundle from CDN -->
103
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
104
+ </body>
105
+ </html>
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: ":memory:"
data/config/routes.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ BetterStructureSql::Engine.routes.draw do
4
+ resources :schema_versions, only: %i[index show] do
5
+ member do
6
+ get :raw
7
+ get :download
8
+ end
9
+ end
10
+
11
+ root to: 'schema_versions#index'
12
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Adapters
5
+ # Base adapter class defining the interface contract for database-specific adapters.
6
+ #
7
+ # All concrete adapters must inherit from this class and implement the abstract methods.
8
+ # This class provides the foundation for database introspection and SQL generation across
9
+ # different database systems (PostgreSQL, MySQL, SQLite).
10
+ #
11
+ # @abstract Subclasses must implement all abstract methods
12
+ class BaseAdapter
13
+ # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
14
+ attr_reader :connection
15
+
16
+ # Initialize a new adapter instance
17
+ #
18
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
19
+ def initialize(connection)
20
+ @connection = connection
21
+ end
22
+
23
+ # Abstract introspection methods - must be implemented by subclasses
24
+
25
+ # Fetch database extensions
26
+ #
27
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
28
+ # @return [Array<Hash>] Array of extension hashes with :name, :version, :schema
29
+ # @raise [NotImplementedError] If not implemented by subclass
30
+ def fetch_extensions(connection)
31
+ raise NotImplementedError, "#{self.class} must implement #fetch_extensions"
32
+ end
33
+
34
+ # Fetch custom types (enums, composite types, domains)
35
+ #
36
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
37
+ # @return [Array<Hash>] Array of type hashes with :name, :type, :schema, and type-specific attributes
38
+ # @raise [NotImplementedError] If not implemented by subclass
39
+ def fetch_custom_types(connection)
40
+ raise NotImplementedError, "#{self.class} must implement #fetch_custom_types"
41
+ end
42
+
43
+ # Fetch tables with columns and constraints
44
+ #
45
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
46
+ # @return [Array<Hash>] Array of table hashes with :name, :schema, :columns, :primary_key, :constraints
47
+ # @raise [NotImplementedError] If not implemented by subclass
48
+ def fetch_tables(connection)
49
+ raise NotImplementedError, "#{self.class} must implement #fetch_tables"
50
+ end
51
+
52
+ # Fetch indexes
53
+ #
54
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
55
+ # @return [Array<Hash>] Array of index hashes with :name, :table, :columns, :unique, :type
56
+ # @raise [NotImplementedError] If not implemented by subclass
57
+ def fetch_indexes(connection)
58
+ raise NotImplementedError, "#{self.class} must implement #fetch_indexes"
59
+ end
60
+
61
+ # Fetch foreign keys
62
+ #
63
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
64
+ # @return [Array<Hash>] Array of foreign key hashes with :table, :name, :column, :foreign_table, :foreign_column, :on_update, :on_delete
65
+ # @raise [NotImplementedError] If not implemented by subclass
66
+ def fetch_foreign_keys(connection)
67
+ raise NotImplementedError, "#{self.class} must implement #fetch_foreign_keys"
68
+ end
69
+
70
+ # Fetch views
71
+ #
72
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
73
+ # @return [Array<Hash>] Array of view hashes with :schema, :name, :definition
74
+ # @raise [NotImplementedError] If not implemented by subclass
75
+ def fetch_views(connection)
76
+ raise NotImplementedError, "#{self.class} must implement #fetch_views"
77
+ end
78
+
79
+ # Fetch materialized views
80
+ #
81
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
82
+ # @return [Array<Hash>] Array of materialized view hashes with :schema, :name, :definition, :indexes
83
+ # @raise [NotImplementedError] If not implemented by subclass
84
+ def fetch_materialized_views(connection)
85
+ raise NotImplementedError, "#{self.class} must implement #fetch_materialized_views"
86
+ end
87
+
88
+ # Fetch functions
89
+ #
90
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
91
+ # @return [Array<Hash>] Array of function hashes with :schema, :name, :definition
92
+ # @raise [NotImplementedError] If not implemented by subclass
93
+ def fetch_functions(connection)
94
+ raise NotImplementedError, "#{self.class} must implement #fetch_functions"
95
+ end
96
+
97
+ # Fetch sequences
98
+ #
99
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
100
+ # @return [Array<Hash>] Array of sequence hashes with :name, :schema, :start_value, :increment
101
+ # @raise [NotImplementedError] If not implemented by subclass
102
+ def fetch_sequences(connection)
103
+ raise NotImplementedError, "#{self.class} must implement #fetch_sequences"
104
+ end
105
+
106
+ # Fetch triggers
107
+ #
108
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
109
+ # @return [Array<Hash>] Array of trigger hashes with :schema, :name, :table_name, :definition
110
+ # @raise [NotImplementedError] If not implemented by subclass
111
+ def fetch_triggers(connection)
112
+ raise NotImplementedError, "#{self.class} must implement #fetch_triggers"
113
+ end
114
+
115
+ # Capability methods - indicate feature support
116
+
117
+ # Indicates whether this database supports extensions (like PostgreSQL extensions)
118
+ #
119
+ # @return [Boolean] True if extensions are supported, false otherwise
120
+ def supports_extensions?
121
+ false
122
+ end
123
+
124
+ # Indicates whether this database supports materialized views
125
+ #
126
+ # @return [Boolean] True if materialized views are supported, false otherwise
127
+ def supports_materialized_views?
128
+ false
129
+ end
130
+
131
+ # Indicates whether this database supports custom types (enums, composite types)
132
+ #
133
+ # @return [Boolean] True if custom types are supported, false otherwise
134
+ def supports_custom_types?
135
+ false
136
+ end
137
+
138
+ # Indicates whether this database supports domains
139
+ #
140
+ # @return [Boolean] True if domains are supported, false otherwise
141
+ def supports_domains?
142
+ false
143
+ end
144
+
145
+ # Indicates whether this database supports stored procedures/functions
146
+ #
147
+ # @return [Boolean] True if functions are supported, false otherwise
148
+ def supports_functions?
149
+ false
150
+ end
151
+
152
+ # Indicates whether this database supports triggers
153
+ #
154
+ # @return [Boolean] True if triggers are supported, false otherwise
155
+ def supports_triggers?
156
+ false
157
+ end
158
+
159
+ # Indicates whether this database supports sequences
160
+ #
161
+ # @return [Boolean] True if sequences are supported, false otherwise
162
+ def supports_sequences?
163
+ false
164
+ end
165
+
166
+ # Version detection
167
+
168
+ # Detect the database version
169
+ #
170
+ # @return [String] Normalized version string (e.g., "14.5" for PostgreSQL 14.5)
171
+ # @raise [NotImplementedError] If not implemented by subclass
172
+ def database_version
173
+ raise NotImplementedError, "#{self.class} must implement #database_version"
174
+ end
175
+
176
+ # Parse version string from database
177
+ #
178
+ # @param version_string [String] Raw version string from database
179
+ # @return [String] Normalized version (e.g., "14.5")
180
+ # @raise [NotImplementedError] If not implemented by subclass
181
+ def parse_version(version_string)
182
+ raise NotImplementedError, "#{self.class} must implement #parse_version"
183
+ end
184
+
185
+ # Utility methods for version comparison
186
+
187
+ # Extract major version number
188
+ #
189
+ # @param version_string [String] Version string (e.g., "14.5")
190
+ # @return [Integer] Major version (e.g., 14)
191
+ def major_version(version_string)
192
+ version_string.split('.').first.to_i
193
+ end
194
+
195
+ # Extract minor version number
196
+ #
197
+ # @param version_string [String] Version string (e.g., "14.5")
198
+ # @return [Integer] Minor version (e.g., 5), or 0 if not present
199
+ def minor_version(version_string)
200
+ parts = version_string.split('.')
201
+ parts.length > 1 ? parts[1].to_i : 0
202
+ end
203
+
204
+ # Compare two version strings
205
+ #
206
+ # @param version1 [String] First version to compare
207
+ # @param version2 [String] Second version to compare
208
+ # @return [Integer] -1 if version1 < version2, 0 if equal, 1 if version1 > version2
209
+ def compare_versions(version1, version2)
210
+ v1_parts = version1.split('.').map(&:to_i)
211
+ v2_parts = version2.split('.').map(&:to_i)
212
+
213
+ [v1_parts.length, v2_parts.length].max.times do |i|
214
+ v1 = v1_parts[i] || 0
215
+ v2 = v2_parts[i] || 0
216
+
217
+ return -1 if v1 < v2
218
+ return 1 if v1 > v2
219
+ end
220
+
221
+ 0
222
+ end
223
+
224
+ # Check if version meets minimum requirement
225
+ #
226
+ # @param current_version [String] Current database version
227
+ # @param required_version [String] Required minimum version
228
+ # @return [Boolean] True if current version is greater than or equal to required version
229
+ def version_at_least?(current_version, required_version)
230
+ compare_versions(current_version, required_version) >= 0
231
+ end
232
+ end
233
+ end
234
+ end