rails2qdbd 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 +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +161 -0
- data/exe/rails2qdbd +6 -0
- data/lib/rails2qdbd/catalog.rb +68 -0
- data/lib/rails2qdbd/cli.rb +95 -0
- data/lib/rails2qdbd/config.rb +61 -0
- data/lib/rails2qdbd/converter.rb +96 -0
- data/lib/rails2qdbd/grouping.rb +23 -0
- data/lib/rails2qdbd/inflector.rb +65 -0
- data/lib/rails2qdbd/literal.rb +80 -0
- data/lib/rails2qdbd/model_scanner.rb +254 -0
- data/lib/rails2qdbd/output.rb +59 -0
- data/lib/rails2qdbd/resolver.rb +266 -0
- data/lib/rails2qdbd/schema_parser.rb +240 -0
- data/lib/rails2qdbd/version.rb +5 -0
- data/lib/rails2qdbd/writer.rb +115 -0
- data/lib/rails2qdbd.rb +26 -0
- metadata +105 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require "active_support/inflector"
|
|
5
|
+
|
|
6
|
+
module Rails2qdbd
|
|
7
|
+
# Reads the db/schema.rb dump DSL statically into a Catalog::Schema.
|
|
8
|
+
class SchemaParser
|
|
9
|
+
include Catalog
|
|
10
|
+
|
|
11
|
+
# Calls inside `create_table` that are not columns.
|
|
12
|
+
TABLE_IGNORED = %i[exclusion_constraint unique_constraint foreign_key].freeze
|
|
13
|
+
COLUMN_OPTIONS = %i[null default limit precision scale array comment].freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :diagnostics
|
|
16
|
+
|
|
17
|
+
# inflector: anything answering #singularize and #pluralize (the project's Inflector later on).
|
|
18
|
+
def initialize(inflector: ActiveSupport::Inflector)
|
|
19
|
+
@inflector = inflector
|
|
20
|
+
@diagnostics = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# paths are relative to root and are reported that way; tables share one namespace.
|
|
24
|
+
def parse_files(paths, root:)
|
|
25
|
+
merged = Schema.new
|
|
26
|
+
paths.each do |path|
|
|
27
|
+
full = File.expand_path(path, root)
|
|
28
|
+
raise Error, "schema file not found: #{path}" unless File.file?(full)
|
|
29
|
+
|
|
30
|
+
schema = parse(File.read(full), path: path)
|
|
31
|
+
merged.version ||= schema.version
|
|
32
|
+
merged.enums.merge!(schema.enums)
|
|
33
|
+
merged.foreign_keys.concat(schema.foreign_keys)
|
|
34
|
+
schema.tables.each do |name, table|
|
|
35
|
+
if (first = merged.tables[name])
|
|
36
|
+
error(%(table "#{name}" already defined at #{first.path}:#{first.line}), path, table.line)
|
|
37
|
+
else
|
|
38
|
+
merged.tables[name] = table
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
merged
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def parse(source, path:)
|
|
46
|
+
@path = path
|
|
47
|
+
@schema = Schema.new
|
|
48
|
+
@pending_enums = []
|
|
49
|
+
result = Prism.parse(source)
|
|
50
|
+
if result.failure?
|
|
51
|
+
result.errors.each { |e| error("syntax error: #{e.message}", path, e.location.start_line) }
|
|
52
|
+
return @schema
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
statements(define_body(result.value)).each { |node| top_level(node) }
|
|
56
|
+
@pending_enums.each { |column, type| column.enum_values = @schema.enums[type] }
|
|
57
|
+
@schema
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# The block of `ActiveRecord::Schema[8.1].define(...)`; falls back to the whole program.
|
|
63
|
+
def define_body(program)
|
|
64
|
+
define = statements(program).find { |n| n.is_a?(Prism::CallNode) && n.name == :define && n.block }
|
|
65
|
+
return program unless define
|
|
66
|
+
|
|
67
|
+
receiver = define.receiver
|
|
68
|
+
if receiver.is_a?(Prism::CallNode) && receiver.name == :[]
|
|
69
|
+
version = Literal.value(receiver.arguments&.arguments&.first)
|
|
70
|
+
@schema.version = version.to_s if Literal.known?(version)
|
|
71
|
+
end
|
|
72
|
+
define.block
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def statements(node)
|
|
76
|
+
body = node.respond_to?(:statements) ? node.statements : node.body
|
|
77
|
+
body = body.body if body.is_a?(Prism::StatementsNode)
|
|
78
|
+
Array(body)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def top_level(node)
|
|
82
|
+
return unless node.is_a?(Prism::CallNode) && node.receiver.nil?
|
|
83
|
+
|
|
84
|
+
args, options = Literal.arguments(node)
|
|
85
|
+
case node.name
|
|
86
|
+
when :create_table then create_table(node, args, options)
|
|
87
|
+
when :add_foreign_key then add_foreign_key(args, options)
|
|
88
|
+
when :add_index then add_index(args, options)
|
|
89
|
+
when :create_enum then create_enum(args)
|
|
90
|
+
end
|
|
91
|
+
# Anything else (enable_extension, execute, create_view, ...) is not drawn.
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def create_table(node, args, options)
|
|
95
|
+
name = literal(args.first)
|
|
96
|
+
return warn("create_table without a literal table name", node) unless name.is_a?(String) || name.is_a?(Symbol)
|
|
97
|
+
|
|
98
|
+
table = Table.new(name: name.to_s, path: @path, line: node.location.start_line)
|
|
99
|
+
table.comment = literal(options[:comment])
|
|
100
|
+
add_primary_key(table, options)
|
|
101
|
+
statements(node.block).each { |statement| table_statement(table, statement) } if node.block&.body
|
|
102
|
+
@schema.tables[table.name] = table
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def add_primary_key(table, options)
|
|
106
|
+
id = options.key?(:id) ? literal(options[:id]) : :bigint
|
|
107
|
+
primary_key = options.key?(:primary_key) ? literal(options[:primary_key]) : "id"
|
|
108
|
+
return if id == false
|
|
109
|
+
|
|
110
|
+
if primary_key.is_a?(Array) # composite: the columns are declared in the block
|
|
111
|
+
table.primary_key = primary_key.map(&:to_s)
|
|
112
|
+
return
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# `id: :uuid`, or `id: { type: :string, limit: 2 }` with column options inline.
|
|
116
|
+
attrs = id.is_a?(Hash) ? id.slice(*COLUMN_OPTIONS).except(:null) : {}
|
|
117
|
+
type = id.is_a?(Hash) ? id.fetch(:type, :bigint) : id
|
|
118
|
+
attrs[:default] = literal(options[:default]) if options.key?(:default)
|
|
119
|
+
table.primary_key = [primary_key.to_s]
|
|
120
|
+
table.columns << Column.new(name: primary_key.to_s, type: type.to_s, null: false, **attrs)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def table_statement(table, node)
|
|
124
|
+
return unless node.is_a?(Prism::CallNode) && node.receiver
|
|
125
|
+
|
|
126
|
+
args, options = Literal.arguments(node)
|
|
127
|
+
case node.name
|
|
128
|
+
when :index then table.indexes << build_index(args.first, options)
|
|
129
|
+
when :references, :belongs_to then add_references(table, args, options)
|
|
130
|
+
when :timestamps then add_timestamps(table, options)
|
|
131
|
+
when :check_constraint then table.checks << literal(args.first).to_s
|
|
132
|
+
when :virtual then add_column(table, node, args.first, literal(options[:type]), options)
|
|
133
|
+
when :column then add_column(table, node, args.first, literal(args[1]), options)
|
|
134
|
+
when *TABLE_IGNORED then nil
|
|
135
|
+
else
|
|
136
|
+
return warn("unrecognized `#{node.name}` in create_table \"#{table.name}\"", node) if args.empty?
|
|
137
|
+
|
|
138
|
+
add_column(table, node, args.first, node.name, options)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def add_column(table, node, name_node, type, options)
|
|
143
|
+
name = literal(name_node)
|
|
144
|
+
return warn("column without a literal name in \"#{table.name}\"", node) unless name.is_a?(String) || name.is_a?(Symbol)
|
|
145
|
+
|
|
146
|
+
attrs = COLUMN_OPTIONS.filter_map { |key| [key, literal(options[key])] if options.key?(key) }.to_h
|
|
147
|
+
column = Column.new(name: name.to_s, type: type.to_s, **attrs)
|
|
148
|
+
column.null = true if column.null.nil?
|
|
149
|
+
column.generated = generated(options) if options.key?(:as)
|
|
150
|
+
if type.to_s == "enum" && options.key?(:enum_type)
|
|
151
|
+
column.type = literal(options[:enum_type]).to_s
|
|
152
|
+
@pending_enums << [column, column.type]
|
|
153
|
+
end
|
|
154
|
+
table.primary_key << column.name if literal(options[:primary_key]) == true
|
|
155
|
+
table.columns << column
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def generated(options)
|
|
159
|
+
expression = literal(options[:as]).to_s
|
|
160
|
+
literal(options[:stored]) == true ? "#{expression} (stored)" : expression
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def add_references(table, args, options)
|
|
164
|
+
null = options.key?(:null) ? literal(options[:null]) : true
|
|
165
|
+
type = options.key?(:type) ? literal(options[:type]).to_s : "bigint"
|
|
166
|
+
polymorphic = literal(options[:polymorphic])
|
|
167
|
+
args.each do |arg|
|
|
168
|
+
name = literal(arg).to_s
|
|
169
|
+
columns = ["#{name}_id"]
|
|
170
|
+
if polymorphic
|
|
171
|
+
table.columns << Column.new(name: "#{name}_type", type: "string", null: null)
|
|
172
|
+
columns.unshift("#{name}_type")
|
|
173
|
+
end
|
|
174
|
+
table.columns << Column.new(name: "#{name}_id", type: type, null: null)
|
|
175
|
+
table.indexes << Index.new(columns: columns) unless literal(options[:index]) == false
|
|
176
|
+
add_reference_fk(table, name, literal(options[:foreign_key])) unless polymorphic
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def add_reference_fk(table, name, foreign_key)
|
|
181
|
+
return unless foreign_key
|
|
182
|
+
|
|
183
|
+
to_table = foreign_key.is_a?(Hash) && foreign_key[:to_table] ? foreign_key[:to_table].to_s : @inflector.pluralize(name)
|
|
184
|
+
@schema.foreign_keys << ForeignKey.new(from_table: table.name, from_column: "#{name}_id", to_table: to_table, to_column: "id")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def add_timestamps(table, options)
|
|
188
|
+
null = options.key?(:null) ? literal(options[:null]) : false
|
|
189
|
+
%w[created_at updated_at].each { |name| table.columns << Column.new(name: name, type: "datetime", null: null) }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def add_foreign_key(args, options)
|
|
193
|
+
from, to = args.map { |node| literal(node).to_s }
|
|
194
|
+
column = options.key?(:column) ? literal(options[:column]).to_s : "#{@inflector.singularize(to)}_id"
|
|
195
|
+
primary_key = options.key?(:primary_key) ? literal(options[:primary_key]).to_s : "id"
|
|
196
|
+
@schema.foreign_keys << ForeignKey.new(from_table: from, from_column: column, to_table: to, to_column: primary_key)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def add_index(args, options)
|
|
200
|
+
table = @schema.tables[literal(args.first).to_s]
|
|
201
|
+
table&.indexes&.<<(build_index(args[1], options))
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Arrays and symbols name columns; a string is a column only if it looks like an identifier.
|
|
205
|
+
def build_index(target, options)
|
|
206
|
+
value = literal(target)
|
|
207
|
+
columns, expression = case value
|
|
208
|
+
when Array then [value.map(&:to_s), nil]
|
|
209
|
+
when Symbol then [[value.to_s], nil]
|
|
210
|
+
when /\A\w+\z/ then [[value], nil]
|
|
211
|
+
else [[], value.to_s]
|
|
212
|
+
end
|
|
213
|
+
Index.new(columns: columns, expression: expression, unique: literal(options[:unique]) == true,
|
|
214
|
+
where: options.key?(:where) ? literal(options[:where]) : nil,
|
|
215
|
+
name: options.key?(:name) ? literal(options[:name]) : nil)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def create_enum(args)
|
|
219
|
+
name, values = args.map { |node| literal(node) }
|
|
220
|
+
@schema.enums[name.to_s] = Array(values).map(&:to_s)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Literal value, or the source text for anything computed (good enough for a diagram).
|
|
224
|
+
def literal(node)
|
|
225
|
+
return nil if node.nil?
|
|
226
|
+
|
|
227
|
+
value = Literal.value(node)
|
|
228
|
+
Literal.known?(value) ? value : node.slice
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def warn(message, node)
|
|
232
|
+
@diagnostics << Diagnostic.new(severity: :warning, message: message, path: @path, line: node.location.start_line)
|
|
233
|
+
nil
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def error(message, path, line)
|
|
237
|
+
@diagnostics << Diagnostic.new(severity: :error, message: message, path: path, line: line)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails2qdbd
|
|
4
|
+
# Renders Catalog tables as QuickDBD text, following qdbd's parser rules.
|
|
5
|
+
class Writer
|
|
6
|
+
HEADER = "# Generated by rails2qdbd"
|
|
7
|
+
IDENTIFIER = /\A[A-Za-z0-9_]+\z/
|
|
8
|
+
LIMITED_TYPES = %w[string text binary char varchar citext].freeze
|
|
9
|
+
OPERATORS = { many_to_one: ">-", one_to_one: "-" }.freeze
|
|
10
|
+
|
|
11
|
+
# Bare when qdbd accepts it as is, else double-quoted (qdbd has no escape for '"').
|
|
12
|
+
def self.quote_identifier(name)
|
|
13
|
+
name = name.to_s
|
|
14
|
+
name.match?(IDENTIFIER) ? name : %("#{name.tr('"', "'")}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The text after `default=`; nil when there is no default to show.
|
|
18
|
+
def self.format_default(value)
|
|
19
|
+
text = case value
|
|
20
|
+
when nil then return nil
|
|
21
|
+
when Array then "[#{value.map { |v| format_default(v).to_s.delete('"') }.join(',')}]"
|
|
22
|
+
when Hash then value.empty? ? "{}" : value.to_s
|
|
23
|
+
else value.to_s
|
|
24
|
+
end
|
|
25
|
+
text.empty? || text.match?(/[\s#"]/) ? %("#{text.tr('"', "'")}") : text
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# One `# ` line per text line; '#' would end qdbd's comment, so it becomes a full-width #.
|
|
29
|
+
def self.comment_lines(text)
|
|
30
|
+
text.to_s.split(/\r?\n/).map { |line| line.split.join(" ").tr("#", "#") }.reject(&:empty?).map { |line| "# #{line}" }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# relationships: { [table, column] => Relationship }; notes are extra documentation lines.
|
|
34
|
+
def render_table(table, relationships: {}, table_notes: [], column_notes: {})
|
|
35
|
+
lines = documentation([table.comment, *table_notes, *index_notes(table), *table.checks.map { |c| "check (#{c})" }])
|
|
36
|
+
lines << self.class.quote_identifier(table.name) << "-"
|
|
37
|
+
ordered_columns(table).each do |column|
|
|
38
|
+
lines.concat(documentation(column_docs(column) + column_notes.fetch(column.name, [])))
|
|
39
|
+
lines << column_line(table, column, relationships[[table.name, column.name]])
|
|
40
|
+
end
|
|
41
|
+
lines.join("\n")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def render_file(tables_text:, inputs:)
|
|
45
|
+
header = ["#{HEADER} #{VERSION} from #{inputs.join(', ')}.",
|
|
46
|
+
"# Re-run the conversion to refresh; manual edits will be overwritten."]
|
|
47
|
+
"#{[header.join("\n"), *tables_text].join("\n\n")}\n"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def documentation(texts) = texts.compact.flat_map { |text| self.class.comment_lines(text) }
|
|
53
|
+
|
|
54
|
+
def ordered_columns(table)
|
|
55
|
+
pk, rest = table.columns.partition { |column| table.primary_key.include?(column.name) }
|
|
56
|
+
pk + rest
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def column_docs(column)
|
|
60
|
+
docs = [column.comment]
|
|
61
|
+
docs << "generated: #{column.generated}" if column.generated
|
|
62
|
+
docs << "enum: #{column.enum_values.join(', ')}" if column.enum_values&.any?
|
|
63
|
+
docs
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def column_line(table, column, relationship)
|
|
67
|
+
pk = table.primary_key.include?(column.name)
|
|
68
|
+
parts = [self.class.quote_identifier(column.name)]
|
|
69
|
+
parts << "PK" if pk
|
|
70
|
+
parts << type(column)
|
|
71
|
+
parts << index_keyword(table, column) unless pk
|
|
72
|
+
parts << "NULL" if column.null && !pk
|
|
73
|
+
default = self.class.format_default(column.default)
|
|
74
|
+
parts << "default=#{default}" if default
|
|
75
|
+
if relationship
|
|
76
|
+
target = "#{self.class.quote_identifier(relationship.to_table)}.#{self.class.quote_identifier(relationship.to_column)}"
|
|
77
|
+
parts << "FK" << OPERATORS.fetch(relationship.cardinality) << target
|
|
78
|
+
end
|
|
79
|
+
parts.compact.join(" ")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# qdbd types are one token without '=', '#' or '"'.
|
|
83
|
+
def type(column)
|
|
84
|
+
base = column.type.to_s.strip.gsub(/\s+/, "_").tr("=", ":").delete('#"')
|
|
85
|
+
base = "unknown" if base.empty?
|
|
86
|
+
size = if column.precision && column.scale then "(#{column.precision},#{column.scale})"
|
|
87
|
+
elsif column.precision then "(#{column.precision})"
|
|
88
|
+
elsif column.limit && LIMITED_TYPES.include?(base) then "(#{column.limit})"
|
|
89
|
+
end
|
|
90
|
+
"#{base}#{size}#{'[]' if column.array}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def index_keyword(table, column)
|
|
94
|
+
single = table.indexes.select { |index| index.single_column? && index.columns.first == column.name }
|
|
95
|
+
if single.any?(&:unique) then "UNIQUE"
|
|
96
|
+
elsif single.any? then "INDEX"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Single-column indexes are column keywords, except on a PK column, which takes none: there it is
|
|
101
|
+
# redundant on a one-column key and noted on a composite one.
|
|
102
|
+
def keyword_index?(table, index)
|
|
103
|
+
index.single_column? && (table.primary_key.size == 1 || !table.primary_key.include?(index.columns.first))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Indexes qdbd cannot express as a column keyword.
|
|
107
|
+
def index_notes(table)
|
|
108
|
+
table.indexes.reject { |index| keyword_index?(table, index) }.map do |index|
|
|
109
|
+
target = index.expression || index.columns.join(", ")
|
|
110
|
+
note = "#{'unique ' if index.unique}index (#{target})"
|
|
111
|
+
index.where ? "#{note} where #{index.where}" : note
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/rails2qdbd.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rails2qdbd/version"
|
|
4
|
+
require_relative "rails2qdbd/catalog"
|
|
5
|
+
require_relative "rails2qdbd/literal"
|
|
6
|
+
require_relative "rails2qdbd/inflector"
|
|
7
|
+
require_relative "rails2qdbd/schema_parser"
|
|
8
|
+
require_relative "rails2qdbd/model_scanner"
|
|
9
|
+
require_relative "rails2qdbd/resolver"
|
|
10
|
+
require_relative "rails2qdbd/writer"
|
|
11
|
+
require_relative "rails2qdbd/config"
|
|
12
|
+
require_relative "rails2qdbd/grouping"
|
|
13
|
+
require_relative "rails2qdbd/converter"
|
|
14
|
+
require_relative "rails2qdbd/output"
|
|
15
|
+
require_relative "rails2qdbd/cli"
|
|
16
|
+
|
|
17
|
+
# Converts a Rails project's db/schema.rb and app/models into qdbd schema files.
|
|
18
|
+
# Pipeline (see Converter): SchemaParser + ModelScanner -> Resolver -> Writer -> Output.
|
|
19
|
+
module Rails2qdbd
|
|
20
|
+
class Error < StandardError; end
|
|
21
|
+
|
|
22
|
+
# Converts the app at app_root. overrides: Config keys (output:, models:, exclude:, ...).
|
|
23
|
+
def self.convert(app_root, config_path: nil, **overrides)
|
|
24
|
+
Converter.new(Config.load(app_root, path: config_path, overrides: overrides)).run
|
|
25
|
+
end
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails2qdbd
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- fastcatch
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '7.1'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: prism
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.0'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '3'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '1.0'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '3'
|
|
52
|
+
description: |
|
|
53
|
+
rails2qdbd reads db/schema.rb and app/models statically (no app boot, no database)
|
|
54
|
+
and writes QuickDBD-syntax schema files for qdbd, the ER-diagram toolkit.
|
|
55
|
+
email:
|
|
56
|
+
- fastcatch@gmail.com
|
|
57
|
+
executables:
|
|
58
|
+
- rails2qdbd
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- CHANGELOG.md
|
|
63
|
+
- LICENSE.txt
|
|
64
|
+
- README.md
|
|
65
|
+
- exe/rails2qdbd
|
|
66
|
+
- lib/rails2qdbd.rb
|
|
67
|
+
- lib/rails2qdbd/catalog.rb
|
|
68
|
+
- lib/rails2qdbd/cli.rb
|
|
69
|
+
- lib/rails2qdbd/config.rb
|
|
70
|
+
- lib/rails2qdbd/converter.rb
|
|
71
|
+
- lib/rails2qdbd/grouping.rb
|
|
72
|
+
- lib/rails2qdbd/inflector.rb
|
|
73
|
+
- lib/rails2qdbd/literal.rb
|
|
74
|
+
- lib/rails2qdbd/model_scanner.rb
|
|
75
|
+
- lib/rails2qdbd/output.rb
|
|
76
|
+
- lib/rails2qdbd/resolver.rb
|
|
77
|
+
- lib/rails2qdbd/schema_parser.rb
|
|
78
|
+
- lib/rails2qdbd/version.rb
|
|
79
|
+
- lib/rails2qdbd/writer.rb
|
|
80
|
+
homepage: https://github.com/fastcatch/rails2qdbd
|
|
81
|
+
licenses:
|
|
82
|
+
- MIT
|
|
83
|
+
metadata:
|
|
84
|
+
source_code_uri: https://github.com/fastcatch/rails2qdbd
|
|
85
|
+
changelog_uri: https://github.com/fastcatch/rails2qdbd/blob/main/CHANGELOG.md
|
|
86
|
+
bug_tracker_uri: https://github.com/fastcatch/rails2qdbd/issues
|
|
87
|
+
rubygems_mfa_required: 'true'
|
|
88
|
+
rdoc_options: []
|
|
89
|
+
require_paths:
|
|
90
|
+
- lib
|
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '3.2'
|
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '0'
|
|
101
|
+
requirements: []
|
|
102
|
+
rubygems_version: 4.0.20
|
|
103
|
+
specification_version: 4
|
|
104
|
+
summary: Convert a Rails app's schema.rb and models into qdbd (QuickDBD) schema files.
|
|
105
|
+
test_files: []
|