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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Generators
5
+ # Generates CREATE TRIGGER statements
6
+ #
7
+ # Supports BEFORE, AFTER, INSTEAD OF triggers with row/statement timing.
8
+ class TriggerGenerator < Base
9
+ # Generates CREATE TRIGGER statement
10
+ #
11
+ # @param trigger [Hash] Trigger metadata
12
+ # @return [String] SQL statement
13
+ def generate(trigger)
14
+ # PostgreSQL's pg_get_triggerdef returns complete CREATE TRIGGER statement
15
+ # MySQL SHOW CREATE TRIGGER also returns complete statement
16
+ if trigger[:definition]
17
+ definition = trigger[:definition].strip
18
+
19
+ # Strip DEFINER clause for MySQL triggers for portability
20
+ definition = definition.gsub(/CREATE DEFINER=`[^`]+`@`[^`]+`/, 'CREATE') if definition.include?('CREATE DEFINER')
21
+
22
+ # Ensure ends with semicolon for structure.sql
23
+ definition += ';' unless definition.end_with?(';')
24
+ return definition
25
+ end
26
+
27
+ # For MySQL/SQLite, generate CREATE TRIGGER from components
28
+ timing = trigger[:timing] || 'AFTER'
29
+ event = trigger[:event] || 'INSERT'
30
+ table_name = quote_identifier(trigger[:table_name])
31
+ trigger_name = quote_identifier(trigger[:name])
32
+ statement = trigger[:statement] || trigger[:body] || ''
33
+
34
+ <<~SQL.strip
35
+ CREATE TRIGGER #{trigger_name}
36
+ #{timing} #{event} ON #{table_name}
37
+ FOR EACH ROW
38
+ BEGIN
39
+ #{statement}
40
+ END;
41
+ SQL
42
+ end
43
+
44
+ private
45
+
46
+ def quote_identifier(identifier)
47
+ return identifier if identifier.nil?
48
+
49
+ # Use double quotes for SQL standard identifier quoting
50
+ "\"#{identifier}\""
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Generators
5
+ # Generates CREATE TYPE statements for enums and composite types
6
+ class TypeGenerator < Base
7
+ # Generates CREATE TYPE statement
8
+ #
9
+ # @param type [Hash] Type metadata (enum or composite)
10
+ # @return [String, nil] SQL statement or nil if unsupported
11
+ def generate(type)
12
+ case type[:type]
13
+ when 'enum'
14
+ generate_enum(type)
15
+ when 'composite'
16
+ generate_composite(type)
17
+ when 'domain'
18
+ generate_domain(type)
19
+ else
20
+ # Unknown type, skip
21
+ nil
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def generate_enum(type)
28
+ values = type[:values].map { |v| "'#{v}'" }.join(', ')
29
+ "CREATE TYPE IF NOT EXISTS #{type[:name]} AS ENUM (#{values});"
30
+ end
31
+
32
+ def generate_composite(type)
33
+ # Composite types have attributes
34
+ attrs = type[:attributes].map do |attr|
35
+ "#{attr[:name]} #{attr[:type]}"
36
+ end.join(', ')
37
+ "CREATE TYPE IF NOT EXISTS #{type[:name]} AS (#{attrs});"
38
+ end
39
+
40
+ def generate_domain(type)
41
+ parts = ["CREATE DOMAIN IF NOT EXISTS #{type[:name]} AS #{type[:base_type]}"]
42
+ parts << type[:constraint] if type[:constraint]
43
+ "#{parts.join(' ')};"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Generators
5
+ # Generates CREATE VIEW statements
6
+ class ViewGenerator < Base
7
+ # Generates CREATE VIEW statement
8
+ #
9
+ # @param view [Hash] View metadata with definition
10
+ # @return [String] SQL statement
11
+ def generate(view)
12
+ # Only add schema prefix for non-default schemas
13
+ # PostgreSQL default: 'public'
14
+ # SQLite default: 'main'
15
+ # MySQL default: current database
16
+ default_schemas = %w[public main]
17
+ schema_prefix = default_schemas.include?(view[:schema]) ? '' : "#{view[:schema]}."
18
+ definition = view[:definition].strip
19
+
20
+ # Ensure definition ends with semicolon
21
+ definition += ';' unless definition.end_with?(';')
22
+
23
+ "CREATE OR REPLACE VIEW #{schema_prefix}#{view[:name]} AS\n#{definition}"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Introspection
5
+ # Introspection module for database extensions
6
+ module Extensions
7
+ # Fetches database extensions
8
+ #
9
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
10
+ # @return [Array<Hash>] Array of extension metadata hashes
11
+ def fetch_extensions(connection)
12
+ adapter = get_adapter(connection)
13
+ adapter.fetch_extensions(connection)
14
+ rescue StandardError => e
15
+ warn "Warning: Failed to fetch extensions: #{e.message}"
16
+ []
17
+ end
18
+
19
+ private
20
+
21
+ def get_adapter(connection)
22
+ @get_adapter ||= Adapters::Registry.adapter_for(
23
+ connection,
24
+ adapter_override: BetterStructureSql.configuration.adapter
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Introspection
5
+ # Introspection module for foreign key constraints
6
+ module ForeignKeys
7
+ # Fetches foreign key constraints
8
+ #
9
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
10
+ # @return [Array<Hash>] Array of foreign key metadata hashes
11
+ def fetch_foreign_keys(connection)
12
+ adapter = get_adapter(connection)
13
+ adapter.fetch_foreign_keys(connection)
14
+ rescue StandardError => e
15
+ warn "Warning: Failed to fetch foreign keys: #{e.message}"
16
+ []
17
+ end
18
+
19
+ private
20
+
21
+ def get_adapter(connection)
22
+ @get_adapter ||= Adapters::Registry.adapter_for(
23
+ connection,
24
+ adapter_override: BetterStructureSql.configuration.adapter
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Introspection
5
+ # Introspection module for database functions
6
+ module Functions
7
+ # Fetches database functions and stored procedures
8
+ #
9
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
10
+ # @return [Array<Hash>] Array of function metadata hashes
11
+ def fetch_functions(connection)
12
+ adapter = get_adapter(connection)
13
+ adapter.fetch_functions(connection)
14
+ rescue StandardError => e
15
+ warn "Warning: Failed to fetch functions: #{e.message}"
16
+ []
17
+ end
18
+
19
+ private
20
+
21
+ def get_adapter(connection)
22
+ @get_adapter ||= Adapters::Registry.adapter_for(
23
+ connection,
24
+ adapter_override: BetterStructureSql.configuration.adapter
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Introspection
5
+ # Introspection module for database indexes
6
+ module Indexes
7
+ # Fetches database indexes
8
+ #
9
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
10
+ # @return [Array<Hash>] Array of index metadata hashes
11
+ def fetch_indexes(connection)
12
+ adapter = get_adapter(connection)
13
+ adapter.fetch_indexes(connection)
14
+ rescue StandardError => e
15
+ warn "Warning: Failed to fetch indexes: #{e.message}"
16
+ []
17
+ end
18
+
19
+ private
20
+
21
+ def get_adapter(connection)
22
+ @get_adapter ||= Adapters::Registry.adapter_for(
23
+ connection,
24
+ adapter_override: BetterStructureSql.configuration.adapter
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Introspection
5
+ # Introspection module for database sequences
6
+ module Sequences
7
+ # Fetches database sequences
8
+ #
9
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
10
+ # @return [Array<Hash>] Array of sequence metadata hashes
11
+ def fetch_sequences(connection)
12
+ adapter = get_adapter(connection)
13
+ adapter.fetch_sequences(connection)
14
+ rescue StandardError => e
15
+ warn "Warning: Failed to fetch sequences: #{e.message}"
16
+ []
17
+ end
18
+
19
+ private
20
+
21
+ def get_adapter(connection)
22
+ @get_adapter ||= Adapters::Registry.adapter_for(
23
+ connection,
24
+ adapter_override: BetterStructureSql.configuration.adapter
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Introspection
5
+ # Introspection module for database tables
6
+ module Tables
7
+ # Fetches database tables
8
+ #
9
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
10
+ # @return [Array<Hash>] Array of table metadata hashes
11
+ def fetch_tables(connection)
12
+ adapter = get_adapter(connection)
13
+ adapter.fetch_tables(connection)
14
+ rescue StandardError => e
15
+ warn "Warning: Failed to fetch tables: #{e.message}"
16
+ []
17
+ end
18
+
19
+ private
20
+
21
+ def get_adapter(connection)
22
+ @get_adapter ||= Adapters::Registry.adapter_for(
23
+ connection,
24
+ adapter_override: BetterStructureSql.configuration.adapter
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Introspection
5
+ # Introspection module for database triggers
6
+ module Triggers
7
+ # Fetches database triggers
8
+ #
9
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
10
+ # @return [Array<Hash>] Array of trigger metadata hashes
11
+ def fetch_triggers(connection)
12
+ adapter = get_adapter(connection)
13
+ adapter.fetch_triggers(connection)
14
+ rescue StandardError => e
15
+ warn "Warning: Failed to fetch triggers: #{e.message}"
16
+ []
17
+ end
18
+
19
+ private
20
+
21
+ def get_adapter(connection)
22
+ @get_adapter ||= Adapters::Registry.adapter_for(
23
+ connection,
24
+ adapter_override: BetterStructureSql.configuration.adapter
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Introspection
5
+ # Introspection module for custom database types
6
+ module Types
7
+ # Fetches custom database types (enums, domains, composite)
8
+ #
9
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
10
+ # @return [Array<Hash>] Array of type metadata hashes
11
+ def fetch_custom_types(connection)
12
+ adapter = get_adapter(connection)
13
+ adapter.fetch_custom_types(connection)
14
+ rescue StandardError => e
15
+ warn "Warning: Failed to fetch custom types: #{e.message}"
16
+ []
17
+ end
18
+
19
+ # Fetches enum types only
20
+ #
21
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
22
+ # @return [Array<Hash>] Array of enum type metadata hashes
23
+ def fetch_enums(connection)
24
+ fetch_custom_types(connection).select { |t| t[:type] == 'enum' }
25
+ end
26
+
27
+ private
28
+
29
+ def get_adapter(connection)
30
+ @get_adapter ||= Adapters::Registry.adapter_for(
31
+ connection,
32
+ adapter_override: BetterStructureSql.configuration.adapter
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Introspection
5
+ # Introspection module for database views
6
+ module Views
7
+ # Fetches database views
8
+ #
9
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
10
+ # @return [Array<Hash>] Array of view metadata hashes
11
+ def fetch_views(connection)
12
+ adapter = get_adapter(connection)
13
+ adapter.fetch_views(connection)
14
+ rescue StandardError => e
15
+ warn "Warning: Failed to fetch views: #{e.message}"
16
+ []
17
+ end
18
+
19
+ # Fetches materialized views
20
+ #
21
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
22
+ # @return [Array<Hash>] Array of materialized view metadata hashes
23
+ def fetch_materialized_views(connection)
24
+ adapter = get_adapter(connection)
25
+ adapter.fetch_materialized_views(connection)
26
+ rescue StandardError => e
27
+ warn "Warning: Failed to fetch materialized views: #{e.message}"
28
+ []
29
+ end
30
+
31
+ private
32
+
33
+ def get_adapter(connection)
34
+ @get_adapter ||= Adapters::Registry.adapter_for(
35
+ connection,
36
+ adapter_override: BetterStructureSql.configuration.adapter
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'introspection/extensions'
4
+ require_relative 'introspection/sequences'
5
+ require_relative 'introspection/types'
6
+ require_relative 'introspection/tables'
7
+ require_relative 'introspection/indexes'
8
+ require_relative 'introspection/foreign_keys'
9
+ require_relative 'introspection/views'
10
+ require_relative 'introspection/functions'
11
+ require_relative 'introspection/triggers'
12
+
13
+ module BetterStructureSql
14
+ # Introspection facade for database metadata extraction
15
+ #
16
+ # Provides a unified interface for querying database objects across
17
+ # all supported adapters (PostgreSQL, MySQL, SQLite).
18
+ module Introspection
19
+ class << self
20
+ include Extensions
21
+ include Sequences
22
+ include Types
23
+ include Tables
24
+ include Indexes
25
+ include ForeignKeys
26
+ include Views
27
+ include Functions
28
+ include Triggers
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module BetterStructureSql
6
+ # Generates manifest JSON files for multi-file schema dumps
7
+ # Provides statistics and metadata about the schema files
8
+ class ManifestGenerator
9
+ attr_reader :config
10
+
11
+ def initialize(config = BetterStructureSql.configuration)
12
+ @config = config
13
+ end
14
+
15
+ # Generate JSON manifest from file map
16
+ # @param file_map [Hash] Map of relative_path => content
17
+ # @return [String] JSON manifest string
18
+ def generate(file_map)
19
+ manifest = {
20
+ version: '1.0',
21
+ total_files: file_map.size,
22
+ total_lines: calculate_total_lines(file_map),
23
+ max_lines_per_file: config.max_lines_per_file,
24
+ directories: calculate_directory_stats(file_map)
25
+ }
26
+
27
+ JSON.pretty_generate(manifest)
28
+ end
29
+
30
+ # Parse manifest JSON string
31
+ # @param json_string [String] The JSON manifest
32
+ # @return [Hash] Parsed manifest data
33
+ def parse(json_string)
34
+ JSON.parse(json_string, symbolize_names: true)
35
+ end
36
+
37
+ private
38
+
39
+ # Calculate total lines across all files
40
+ # @param file_map [Hash] Map of relative_path => content
41
+ # @return [Integer] Total line count
42
+ def calculate_total_lines(file_map)
43
+ file_map.values.sum { |content| content.lines.count }
44
+ end
45
+
46
+ # Calculate statistics per directory
47
+ # @param file_map [Hash] Map of relative_path => content
48
+ # @return [Hash] Directory name => {files:, lines:}
49
+ def calculate_directory_stats(file_map)
50
+ stats = Hash.new { |h, k| h[k] = { files: 0, lines: 0 } }
51
+
52
+ file_map.each do |path, content|
53
+ # Extract directory name (e.g., "1_extensions" from "1_extensions/000001.sql")
54
+ directory = path.split('/').first
55
+ next unless directory
56
+
57
+ stats[directory][:files] += 1
58
+ stats[directory][:lines] += content.lines.count
59
+ end
60
+
61
+ # Sort by directory name to ensure consistent ordering
62
+ stats.sort.to_h
63
+ end
64
+ end
65
+ end