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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +41 -0
- data/LICENSE +21 -0
- data/README.md +557 -0
- data/app/controllers/better_structure_sql/application_controller.rb +61 -0
- data/app/controllers/better_structure_sql/schema_versions_controller.rb +243 -0
- data/app/helpers/better_structure_sql/schema_versions_helper.rb +46 -0
- data/app/views/better_structure_sql/schema_versions/index.html.erb +110 -0
- data/app/views/better_structure_sql/schema_versions/show.html.erb +186 -0
- data/app/views/layouts/better_structure_sql/application.html.erb +105 -0
- data/config/database.yml +3 -0
- data/config/routes.rb +12 -0
- data/lib/better_structure_sql/adapters/base_adapter.rb +234 -0
- data/lib/better_structure_sql/adapters/mysql_adapter.rb +476 -0
- data/lib/better_structure_sql/adapters/mysql_config.rb +32 -0
- data/lib/better_structure_sql/adapters/postgresql_adapter.rb +646 -0
- data/lib/better_structure_sql/adapters/postgresql_config.rb +25 -0
- data/lib/better_structure_sql/adapters/registry.rb +115 -0
- data/lib/better_structure_sql/adapters/sqlite_adapter.rb +644 -0
- data/lib/better_structure_sql/adapters/sqlite_config.rb +26 -0
- data/lib/better_structure_sql/configuration.rb +129 -0
- data/lib/better_structure_sql/database_version.rb +46 -0
- data/lib/better_structure_sql/dependency_resolver.rb +63 -0
- data/lib/better_structure_sql/dumper.rb +544 -0
- data/lib/better_structure_sql/engine.rb +28 -0
- data/lib/better_structure_sql/file_writer.rb +180 -0
- data/lib/better_structure_sql/formatter.rb +70 -0
- data/lib/better_structure_sql/generators/base.rb +33 -0
- data/lib/better_structure_sql/generators/domain_generator.rb +22 -0
- data/lib/better_structure_sql/generators/extension_generator.rb +23 -0
- data/lib/better_structure_sql/generators/foreign_key_generator.rb +43 -0
- data/lib/better_structure_sql/generators/function_generator.rb +33 -0
- data/lib/better_structure_sql/generators/index_generator.rb +50 -0
- data/lib/better_structure_sql/generators/materialized_view_generator.rb +31 -0
- data/lib/better_structure_sql/generators/pragma_generator.rb +23 -0
- data/lib/better_structure_sql/generators/sequence_generator.rb +27 -0
- data/lib/better_structure_sql/generators/table_generator.rb +126 -0
- data/lib/better_structure_sql/generators/trigger_generator.rb +54 -0
- data/lib/better_structure_sql/generators/type_generator.rb +47 -0
- data/lib/better_structure_sql/generators/view_generator.rb +27 -0
- data/lib/better_structure_sql/introspection/extensions.rb +29 -0
- data/lib/better_structure_sql/introspection/foreign_keys.rb +29 -0
- data/lib/better_structure_sql/introspection/functions.rb +29 -0
- data/lib/better_structure_sql/introspection/indexes.rb +29 -0
- data/lib/better_structure_sql/introspection/sequences.rb +29 -0
- data/lib/better_structure_sql/introspection/tables.rb +29 -0
- data/lib/better_structure_sql/introspection/triggers.rb +29 -0
- data/lib/better_structure_sql/introspection/types.rb +37 -0
- data/lib/better_structure_sql/introspection/views.rb +41 -0
- data/lib/better_structure_sql/introspection.rb +31 -0
- data/lib/better_structure_sql/manifest_generator.rb +65 -0
- data/lib/better_structure_sql/migration_patch.rb +196 -0
- data/lib/better_structure_sql/pg_version.rb +44 -0
- data/lib/better_structure_sql/railtie.rb +124 -0
- data/lib/better_structure_sql/schema_loader.rb +168 -0
- data/lib/better_structure_sql/schema_version.rb +86 -0
- data/lib/better_structure_sql/schema_versions.rb +213 -0
- data/lib/better_structure_sql/version.rb +5 -0
- data/lib/better_structure_sql/zip_generator.rb +81 -0
- data/lib/better_structure_sql.rb +81 -0
- data/lib/generators/better_structure_sql/install_generator.rb +44 -0
- data/lib/generators/better_structure_sql/migration_generator.rb +34 -0
- data/lib/generators/better_structure_sql/templates/README +49 -0
- data/lib/generators/better_structure_sql/templates/add_metadata_migration.rb.erb +25 -0
- data/lib/generators/better_structure_sql/templates/better_structure_sql.rb +46 -0
- data/lib/generators/better_structure_sql/templates/migration.rb.erb +26 -0
- data/lib/tasks/better_structure_sql.rake +190 -0
- metadata +299 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterStructureSql
|
|
4
|
+
# Schema version storage and retrieval operations
|
|
5
|
+
#
|
|
6
|
+
# Provides class methods for storing, querying, and managing
|
|
7
|
+
# schema versions with automatic retention cleanup.
|
|
8
|
+
module SchemaVersions
|
|
9
|
+
class << self
|
|
10
|
+
# Store current schema from file
|
|
11
|
+
def store_current(connection = ActiveRecord::Base.connection)
|
|
12
|
+
config = BetterStructureSql.configuration
|
|
13
|
+
pg_version = PgVersion.detect(connection)
|
|
14
|
+
|
|
15
|
+
# Determine format and output mode
|
|
16
|
+
format_type = deduce_format_type(config.output_path)
|
|
17
|
+
output_mode = detect_output_mode(config.output_path)
|
|
18
|
+
|
|
19
|
+
# Read content
|
|
20
|
+
content, zip_archive, file_count = read_schema_content(config.output_path, output_mode)
|
|
21
|
+
|
|
22
|
+
return nil unless content
|
|
23
|
+
|
|
24
|
+
store(
|
|
25
|
+
content: content,
|
|
26
|
+
zip_archive: zip_archive,
|
|
27
|
+
format_type: format_type,
|
|
28
|
+
output_mode: output_mode,
|
|
29
|
+
pg_version: pg_version,
|
|
30
|
+
file_count: file_count,
|
|
31
|
+
connection: connection
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Store schema version with explicit parameters
|
|
36
|
+
def store(content:, format_type:, pg_version:, **options)
|
|
37
|
+
connection = options.fetch(:connection, ActiveRecord::Base.connection)
|
|
38
|
+
output_mode = options.fetch(:output_mode, 'single_file')
|
|
39
|
+
zip_archive = options[:zip_archive]
|
|
40
|
+
file_count = options[:file_count]
|
|
41
|
+
|
|
42
|
+
ensure_table_exists!(connection)
|
|
43
|
+
|
|
44
|
+
version = SchemaVersion.create!(
|
|
45
|
+
content: content,
|
|
46
|
+
zip_archive: zip_archive,
|
|
47
|
+
format_type: format_type,
|
|
48
|
+
output_mode: output_mode,
|
|
49
|
+
pg_version: pg_version,
|
|
50
|
+
file_count: file_count,
|
|
51
|
+
created_at: Time.current
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
cleanup!(connection)
|
|
55
|
+
|
|
56
|
+
version
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Retrieval methods
|
|
60
|
+
def latest
|
|
61
|
+
return nil unless table_exists?
|
|
62
|
+
|
|
63
|
+
SchemaVersion.latest
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns all stored schema versions
|
|
67
|
+
#
|
|
68
|
+
# @return [Array<SchemaVersion>] All versions ordered by creation date (newest first)
|
|
69
|
+
def all_versions
|
|
70
|
+
return [] unless table_exists?
|
|
71
|
+
|
|
72
|
+
SchemaVersion.order(created_at: :desc).to_a
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Finds schema version by ID
|
|
76
|
+
#
|
|
77
|
+
# @param id [Integer] Version ID
|
|
78
|
+
# @return [SchemaVersion, nil] Found version or nil
|
|
79
|
+
def find(id)
|
|
80
|
+
return nil unless table_exists?
|
|
81
|
+
|
|
82
|
+
SchemaVersion.find_by(id: id)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns total count of stored versions
|
|
86
|
+
#
|
|
87
|
+
# @return [Integer] Version count
|
|
88
|
+
def count
|
|
89
|
+
return 0 unless table_exists?
|
|
90
|
+
|
|
91
|
+
SchemaVersion.count
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns versions filtered by format type
|
|
95
|
+
#
|
|
96
|
+
# @param format_type [String] Format type ('sql' or 'rb')
|
|
97
|
+
# @return [Array<SchemaVersion>] Matching versions
|
|
98
|
+
def by_format(format_type)
|
|
99
|
+
return [] unless table_exists?
|
|
100
|
+
|
|
101
|
+
SchemaVersion.by_format(format_type).order(created_at: :desc).to_a
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Retention management
|
|
105
|
+
def cleanup!(_connection = ActiveRecord::Base.connection)
|
|
106
|
+
return 0 unless table_exists?
|
|
107
|
+
|
|
108
|
+
config = BetterStructureSql.configuration
|
|
109
|
+
limit = config.schema_versions_limit
|
|
110
|
+
|
|
111
|
+
# Skip cleanup if unlimited (0)
|
|
112
|
+
return 0 if limit.zero?
|
|
113
|
+
|
|
114
|
+
# Delete oldest versions beyond limit
|
|
115
|
+
total_count = SchemaVersion.count
|
|
116
|
+
return 0 if total_count <= limit
|
|
117
|
+
|
|
118
|
+
versions_to_delete = total_count - limit
|
|
119
|
+
oldest_versions = SchemaVersion.oldest_first.limit(versions_to_delete)
|
|
120
|
+
|
|
121
|
+
deleted_count = 0
|
|
122
|
+
oldest_versions.each do |version|
|
|
123
|
+
version.destroy
|
|
124
|
+
deleted_count += 1
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
deleted_count
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def read_schema_content(output_path, output_mode)
|
|
133
|
+
case output_mode
|
|
134
|
+
when 'single_file'
|
|
135
|
+
full_path = Rails.root.join(output_path)
|
|
136
|
+
return [nil, nil, nil] unless File.exist?(full_path)
|
|
137
|
+
|
|
138
|
+
content = File.read(full_path)
|
|
139
|
+
[content, nil, nil]
|
|
140
|
+
|
|
141
|
+
when 'multi_file'
|
|
142
|
+
full_path = Rails.root.join(output_path)
|
|
143
|
+
return [nil, nil, nil] unless Dir.exist?(full_path)
|
|
144
|
+
|
|
145
|
+
# Read all files and combine into single content
|
|
146
|
+
content = read_multi_file_content(full_path)
|
|
147
|
+
|
|
148
|
+
# Create ZIP archive from directory
|
|
149
|
+
zip_archive = ZipGenerator.create_from_directory(full_path)
|
|
150
|
+
|
|
151
|
+
# Count files
|
|
152
|
+
file_count = Dir.glob("#{full_path}/**/*.sql").count
|
|
153
|
+
|
|
154
|
+
[content, zip_archive, file_count]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def read_multi_file_content(base_path)
|
|
159
|
+
content_parts = []
|
|
160
|
+
|
|
161
|
+
# Read header
|
|
162
|
+
header_path = base_path.join('_header.sql')
|
|
163
|
+
content_parts << File.read(header_path) if File.exist?(header_path)
|
|
164
|
+
|
|
165
|
+
# Read manifest and embed as SQL comment for later extraction
|
|
166
|
+
manifest_path = base_path.join('_manifest.json')
|
|
167
|
+
if File.exist?(manifest_path)
|
|
168
|
+
manifest_json = File.read(manifest_path)
|
|
169
|
+
content_parts << "-- MANIFEST_JSON_START\n-- #{manifest_json.gsub("\n", "\n-- ")}\n-- MANIFEST_JSON_END"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Read numbered directories in order (01_ through 10_)
|
|
173
|
+
# Use pattern that works with Dir.glob
|
|
174
|
+
Dir.glob(File.join(base_path, '*_*')).select { |f| File.directory?(f) }.sort.each do |dir|
|
|
175
|
+
Dir.glob(File.join(dir, '*.sql')).sort.each do |file|
|
|
176
|
+
content_parts << File.read(file)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
content_parts.join("\n\n")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def detect_output_mode(path)
|
|
184
|
+
# No extension → directory → multi_file
|
|
185
|
+
# Has extension (.sql, .rb) → file → single_file
|
|
186
|
+
File.extname(path.to_s).empty? ? 'multi_file' : 'single_file'
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def deduce_format_type(path)
|
|
190
|
+
# Deduce format from file extension in output_path
|
|
191
|
+
if path.to_s.end_with?('.rb')
|
|
192
|
+
'rb'
|
|
193
|
+
else
|
|
194
|
+
'sql' # Default to SQL for .sql or any other extension
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def ensure_table_exists!(_connection)
|
|
199
|
+
return if table_exists?
|
|
200
|
+
|
|
201
|
+
raise Error, "Schema versions table does not exist. Run migration first:\n " \
|
|
202
|
+
"rails generate better_structure_sql:migration\n " \
|
|
203
|
+
'rails db:migrate'
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def table_exists?
|
|
207
|
+
ActiveRecord::Base.connection.table_exists?('better_structure_sql_schema_versions')
|
|
208
|
+
rescue ActiveRecord::NoDatabaseError
|
|
209
|
+
false
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'zip'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
|
|
6
|
+
module BetterStructureSql
|
|
7
|
+
# Handles creating and extracting ZIP archives for multi-file schema dumps
|
|
8
|
+
class ZipGenerator
|
|
9
|
+
MAX_FILES_IN_ZIP = 300_000
|
|
10
|
+
MAX_UNCOMPRESSED_SIZE = 800.megabytes
|
|
11
|
+
|
|
12
|
+
class ZipError < StandardError; end
|
|
13
|
+
|
|
14
|
+
# Create ZIP from existing directory
|
|
15
|
+
# @param dir_path [String, Pathname] The directory path
|
|
16
|
+
# @return [String] ZIP file binary data
|
|
17
|
+
def self.create_from_directory(dir_path)
|
|
18
|
+
buffer = Zip::OutputStream.write_buffer do |zip|
|
|
19
|
+
Dir.glob("#{dir_path}/**/*").sort.each do |file_path|
|
|
20
|
+
next if File.directory?(file_path)
|
|
21
|
+
|
|
22
|
+
relative_path = file_path.sub("#{dir_path}/", '')
|
|
23
|
+
zip.put_next_entry(relative_path)
|
|
24
|
+
zip.write File.read(file_path)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
buffer.string
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Create ZIP from file map (path => content hash)
|
|
32
|
+
# @param file_map [Hash<String, String>] Hash of relative paths to content
|
|
33
|
+
# @return [String] ZIP file binary data
|
|
34
|
+
def self.create_from_file_map(file_map)
|
|
35
|
+
buffer = Zip::OutputStream.write_buffer do |zip|
|
|
36
|
+
file_map.sort.each do |path, content|
|
|
37
|
+
zip.put_next_entry(path)
|
|
38
|
+
zip.write content
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
buffer.string
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Extract ZIP to directory
|
|
46
|
+
# @param zip_binary [String] ZIP file binary data
|
|
47
|
+
# @param target_dir [String, Pathname] Target directory path
|
|
48
|
+
def self.extract_to_directory(zip_binary, target_dir)
|
|
49
|
+
FileUtils.mkdir_p(target_dir)
|
|
50
|
+
|
|
51
|
+
Zip::File.open_buffer(StringIO.new(zip_binary)) do |zip_file|
|
|
52
|
+
zip_file.each do |entry|
|
|
53
|
+
# Security: rubyzip already prevents path traversal, but verify anyway
|
|
54
|
+
next if entry.name.include?('..')
|
|
55
|
+
|
|
56
|
+
path = File.join(target_dir, entry.name)
|
|
57
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
58
|
+
entry.extract(path) unless File.exist?(path)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Validate ZIP safety (prevent ZIP bombs)
|
|
64
|
+
# @param zip_binary [String] ZIP file binary data
|
|
65
|
+
# @raise [ZipError] if ZIP exceeds safety limits
|
|
66
|
+
def self.validate_zip!(zip_binary)
|
|
67
|
+
file_count = 0
|
|
68
|
+
total_size = 0
|
|
69
|
+
|
|
70
|
+
Zip::File.open_buffer(StringIO.new(zip_binary)) do |zip_file|
|
|
71
|
+
zip_file.each do |entry|
|
|
72
|
+
file_count += 1
|
|
73
|
+
total_size += entry.size
|
|
74
|
+
|
|
75
|
+
raise ZipError, "Too many files in ZIP (max #{MAX_FILES_IN_ZIP})" if file_count > MAX_FILES_IN_ZIP
|
|
76
|
+
raise ZipError, "ZIP content too large (max #{MAX_UNCOMPRESSED_SIZE} bytes)" if total_size > MAX_UNCOMPRESSED_SIZE
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_record'
|
|
4
|
+
require_relative 'better_structure_sql/version'
|
|
5
|
+
require_relative 'better_structure_sql/adapters/base_adapter'
|
|
6
|
+
require_relative 'better_structure_sql/adapters/postgresql_config'
|
|
7
|
+
require_relative 'better_structure_sql/adapters/registry'
|
|
8
|
+
require_relative 'better_structure_sql/configuration'
|
|
9
|
+
require_relative 'better_structure_sql/dependency_resolver'
|
|
10
|
+
require_relative 'better_structure_sql/introspection'
|
|
11
|
+
require_relative 'better_structure_sql/formatter'
|
|
12
|
+
require_relative 'better_structure_sql/generators/base'
|
|
13
|
+
require_relative 'better_structure_sql/generators/extension_generator'
|
|
14
|
+
require_relative 'better_structure_sql/generators/type_generator'
|
|
15
|
+
require_relative 'better_structure_sql/generators/sequence_generator'
|
|
16
|
+
require_relative 'better_structure_sql/generators/table_generator'
|
|
17
|
+
require_relative 'better_structure_sql/generators/index_generator'
|
|
18
|
+
require_relative 'better_structure_sql/generators/foreign_key_generator'
|
|
19
|
+
require_relative 'better_structure_sql/generators/view_generator'
|
|
20
|
+
require_relative 'better_structure_sql/generators/materialized_view_generator'
|
|
21
|
+
require_relative 'better_structure_sql/generators/function_generator'
|
|
22
|
+
require_relative 'better_structure_sql/generators/trigger_generator'
|
|
23
|
+
require_relative 'better_structure_sql/generators/domain_generator'
|
|
24
|
+
require_relative 'better_structure_sql/database_version'
|
|
25
|
+
require_relative 'better_structure_sql/pg_version'
|
|
26
|
+
require_relative 'better_structure_sql/schema_version'
|
|
27
|
+
require_relative 'better_structure_sql/schema_versions'
|
|
28
|
+
require_relative 'better_structure_sql/file_writer'
|
|
29
|
+
require_relative 'better_structure_sql/manifest_generator'
|
|
30
|
+
require_relative 'better_structure_sql/zip_generator'
|
|
31
|
+
require_relative 'better_structure_sql/schema_loader'
|
|
32
|
+
require_relative 'better_structure_sql/migration_patch'
|
|
33
|
+
require_relative 'better_structure_sql/dumper'
|
|
34
|
+
require_relative 'better_structure_sql/railtie' if defined?(Rails::Railtie)
|
|
35
|
+
require_relative 'better_structure_sql/engine' if defined?(Rails::Engine)
|
|
36
|
+
|
|
37
|
+
# BetterStructureSql - Clean PostgreSQL schema dumps for Rails applications
|
|
38
|
+
#
|
|
39
|
+
# Replaces noisy structure.sql files with deterministic, maintainable output
|
|
40
|
+
# using pure Ruby database introspection. Supports PostgreSQL, MySQL, and SQLite.
|
|
41
|
+
module BetterStructureSql
|
|
42
|
+
# Base error class for all BetterStructureSql errors
|
|
43
|
+
class Error < StandardError; end
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
attr_writer :configuration
|
|
47
|
+
|
|
48
|
+
# Returns the current configuration instance
|
|
49
|
+
#
|
|
50
|
+
# @return [Configuration] The configuration object
|
|
51
|
+
def configuration
|
|
52
|
+
@configuration ||= Configuration.new
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Configures BetterStructureSql with a block
|
|
56
|
+
#
|
|
57
|
+
# @yield [Configuration] The configuration object
|
|
58
|
+
# @example
|
|
59
|
+
# BetterStructureSql.configure do |config|
|
|
60
|
+
# config.output_path = 'db/structure'
|
|
61
|
+
# config.include_extensions = true
|
|
62
|
+
# end
|
|
63
|
+
def configure
|
|
64
|
+
yield(configuration)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Resets configuration to default values
|
|
68
|
+
#
|
|
69
|
+
# @return [Configuration] A new configuration instance
|
|
70
|
+
def reset_configuration
|
|
71
|
+
@configuration = Configuration.new
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check if BetterStructureSql has been configured
|
|
75
|
+
#
|
|
76
|
+
# @return [Boolean] True if configuration has been set
|
|
77
|
+
def configured?
|
|
78
|
+
!@configuration.nil?
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators/base'
|
|
4
|
+
|
|
5
|
+
module BetterStructureSql
|
|
6
|
+
module Generators
|
|
7
|
+
# Rails generator for installing BetterStructureSql
|
|
8
|
+
#
|
|
9
|
+
# Creates initializer and optionally generates migration for schema_versions table.
|
|
10
|
+
class InstallGenerator < Rails::Generators::Base
|
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
|
12
|
+
|
|
13
|
+
desc 'Creates BetterStructureSql initializer and optionally generates migration'
|
|
14
|
+
|
|
15
|
+
class_option :skip_migration,
|
|
16
|
+
type: :boolean,
|
|
17
|
+
default: false,
|
|
18
|
+
desc: 'Skip migration generation for schema versions table'
|
|
19
|
+
|
|
20
|
+
# Copies initializer template to config/initializers
|
|
21
|
+
#
|
|
22
|
+
# @return [void]
|
|
23
|
+
def copy_initializer
|
|
24
|
+
template 'better_structure_sql.rb', 'config/initializers/better_structure_sql.rb'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Creates migration for schema_versions table
|
|
28
|
+
#
|
|
29
|
+
# @return [void]
|
|
30
|
+
def create_migration
|
|
31
|
+
return if options[:skip_migration]
|
|
32
|
+
|
|
33
|
+
generate 'better_structure_sql:migration'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Displays README after installation
|
|
37
|
+
#
|
|
38
|
+
# @return [void]
|
|
39
|
+
def show_readme
|
|
40
|
+
readme 'README' if behavior == :invoke
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/active_record'
|
|
5
|
+
|
|
6
|
+
module BetterStructureSql
|
|
7
|
+
module Generators
|
|
8
|
+
# Rails generator for creating schema_versions table migration
|
|
9
|
+
class MigrationGenerator < Rails::Generators::Base
|
|
10
|
+
include ActiveRecord::Generators::Migration
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path('templates', __dir__)
|
|
13
|
+
|
|
14
|
+
desc 'Creates migration for BetterStructureSql schema versions table'
|
|
15
|
+
|
|
16
|
+
# Creates migration file for schema_versions table
|
|
17
|
+
#
|
|
18
|
+
# @return [void]
|
|
19
|
+
def create_migration_file
|
|
20
|
+
migration_template(
|
|
21
|
+
'migration.rb.erb',
|
|
22
|
+
'db/migrate/create_better_structure_sql_schema_versions.rb',
|
|
23
|
+
migration_version: migration_version
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def migration_version
|
|
30
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
BetterStructureSql has been installed!
|
|
4
|
+
|
|
5
|
+
Configuration file created at:
|
|
6
|
+
config/initializers/better_structure_sql.rb
|
|
7
|
+
|
|
8
|
+
To generate a clean schema dump, run:
|
|
9
|
+
rails db:schema:dump_better
|
|
10
|
+
|
|
11
|
+
To replace Rails' default schema dump task, set in the initializer:
|
|
12
|
+
config.replace_default_dump = true
|
|
13
|
+
|
|
14
|
+
Then you can use the standard command:
|
|
15
|
+
rails db:schema:dump
|
|
16
|
+
|
|
17
|
+
Phase 1 features available:
|
|
18
|
+
✓ Tables with columns, primary keys, and constraints
|
|
19
|
+
✓ Indexes (including unique and partial)
|
|
20
|
+
✓ Foreign keys with CASCADE/RESTRICT/SET NULL
|
|
21
|
+
✓ PostgreSQL extensions
|
|
22
|
+
✓ Clean, deterministic output
|
|
23
|
+
|
|
24
|
+
Phase 2 features available:
|
|
25
|
+
✓ Schema version storage
|
|
26
|
+
✓ Version retention management
|
|
27
|
+
✓ PostgreSQL version tracking
|
|
28
|
+
|
|
29
|
+
To enable schema versioning:
|
|
30
|
+
1. Enable in config/initializers/better_structure_sql.rb:
|
|
31
|
+
config.enable_schema_versions = true
|
|
32
|
+
2. Run the migration (if not already generated):
|
|
33
|
+
rails db:migrate
|
|
34
|
+
3. Store current schema:
|
|
35
|
+
rails db:schema:store
|
|
36
|
+
4. List versions:
|
|
37
|
+
rails db:schema:versions
|
|
38
|
+
5. Cleanup old versions:
|
|
39
|
+
rails db:schema:cleanup
|
|
40
|
+
|
|
41
|
+
Coming in Phase 3:
|
|
42
|
+
- Views and materialized views
|
|
43
|
+
- Functions and triggers
|
|
44
|
+
- Advanced PostgreSQL features
|
|
45
|
+
|
|
46
|
+
For more information, visit:
|
|
47
|
+
https://github.com/example/better_structure_sql
|
|
48
|
+
|
|
49
|
+
===============================================================================
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class AddMetadataToBetterStructureSqlSchemaVersions < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
# Add new columns
|
|
4
|
+
add_column :better_structure_sql_schema_versions, :content_size, :bigint
|
|
5
|
+
add_column :better_structure_sql_schema_versions, :line_count, :integer
|
|
6
|
+
|
|
7
|
+
# Backfill existing records
|
|
8
|
+
BetterStructureSql::SchemaVersion.reset_column_information
|
|
9
|
+
BetterStructureSql::SchemaVersion.find_each do |version|
|
|
10
|
+
version.update_columns(
|
|
11
|
+
content_size: version.content.bytesize,
|
|
12
|
+
line_count: version.content.lines.count
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Make columns non-nullable
|
|
17
|
+
change_column_null :better_structure_sql_schema_versions, :content_size, false
|
|
18
|
+
change_column_null :better_structure_sql_schema_versions, :line_count, false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def down
|
|
22
|
+
remove_column :better_structure_sql_schema_versions, :line_count
|
|
23
|
+
remove_column :better_structure_sql_schema_versions, :content_size
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
BetterStructureSql.configure do |config|
|
|
4
|
+
# Output path for structure dump
|
|
5
|
+
# Single file: 'db/structure.sql'
|
|
6
|
+
# Multi-file: 'db/schema' (directory) - RECOMMENDED for large projects
|
|
7
|
+
# Benefits: better git diffs, easier navigation, AI-friendly organization
|
|
8
|
+
# NOTE: BetterStructureSql only supports SQL format dumps (structure.sql).
|
|
9
|
+
# Using 'db/schema.rb' will skip replacement of dump/load tasks (can still store versions).
|
|
10
|
+
config.output_path = Rails.root.join('db/structure.sql')
|
|
11
|
+
|
|
12
|
+
# Schema search path (PostgreSQL only)
|
|
13
|
+
config.search_path = 'public'
|
|
14
|
+
|
|
15
|
+
# Feature toggles - what to include in dumps
|
|
16
|
+
# Features auto-skip if not supported by your database
|
|
17
|
+
config.include_extensions = true # PostgreSQL only
|
|
18
|
+
config.include_custom_types = true # PostgreSQL (ENUM, composite), MySQL (ENUM/SET)
|
|
19
|
+
config.include_domains = true # PostgreSQL only
|
|
20
|
+
config.include_sequences = true # PostgreSQL only
|
|
21
|
+
config.include_functions = true # PostgreSQL, MySQL (stored procedures)
|
|
22
|
+
config.include_triggers = true # All databases
|
|
23
|
+
config.include_views = true # All databases
|
|
24
|
+
config.include_materialized_views = true # PostgreSQL only
|
|
25
|
+
|
|
26
|
+
# Output formatting
|
|
27
|
+
config.add_section_spacing = true # Add blank lines between sections
|
|
28
|
+
config.sort_tables = false # Sort tables alphabetically (or use dependency order)
|
|
29
|
+
|
|
30
|
+
# Multi-file output settings (only used when output_path is a directory)
|
|
31
|
+
config.max_lines_per_file = 500 # Target lines per file (soft limit)
|
|
32
|
+
config.overflow_threshold = 1.1 # Allow files to be up to 10% larger to avoid tiny files
|
|
33
|
+
config.generate_manifest = true # Generate _manifest.json with statistics
|
|
34
|
+
|
|
35
|
+
# Schema versioning (stores schema history in database for rollback/comparison)
|
|
36
|
+
config.enable_schema_versions = true # Store versions in database
|
|
37
|
+
config.schema_versions_limit = 10 # Keep last 10 versions (0 = unlimited)
|
|
38
|
+
|
|
39
|
+
# Replace default Rails schema dump/load tasks (opt-in)
|
|
40
|
+
# When true, db:schema:dump and db:schema:load will use BetterStructureSql automatically
|
|
41
|
+
# This also automatically sets config.active_record.schema_format = :sql
|
|
42
|
+
# When false (default), use explicit tasks: db:schema:dump_better and db:schema:load_better
|
|
43
|
+
# NOTE: Only works with SQL format. Silently ignored if output_path ends with '.rb'
|
|
44
|
+
config.replace_default_dump = false
|
|
45
|
+
config.replace_default_load = false
|
|
46
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class CreateBetterStructureSqlSchemaVersions < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :better_structure_sql_schema_versions do |t|
|
|
4
|
+
t.text :content, null: false
|
|
5
|
+
t.binary :zip_archive, null: true
|
|
6
|
+
t.string :pg_version, null: false
|
|
7
|
+
t.string :format_type, null: false
|
|
8
|
+
t.string :output_mode, null: false
|
|
9
|
+
t.bigint :content_size, null: false
|
|
10
|
+
t.integer :line_count, null: false
|
|
11
|
+
t.integer :file_count, null: true
|
|
12
|
+
t.timestamp :created_at, null: false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
add_index :better_structure_sql_schema_versions, :created_at, order: { created_at: :desc }
|
|
16
|
+
add_index :better_structure_sql_schema_versions, :output_mode
|
|
17
|
+
|
|
18
|
+
add_check_constraint :better_structure_sql_schema_versions,
|
|
19
|
+
"format_type IN ('sql', 'rb')",
|
|
20
|
+
name: 'format_type_check'
|
|
21
|
+
|
|
22
|
+
add_check_constraint :better_structure_sql_schema_versions,
|
|
23
|
+
"output_mode IN ('single_file', 'multi_file')",
|
|
24
|
+
name: 'output_mode_check'
|
|
25
|
+
end
|
|
26
|
+
end
|