rails-schema 0.1.1 → 0.1.2
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 +4 -4
- data/CHANGELOG.md +35 -0
- data/PROJECT.md +41 -15
- data/README.md +22 -2
- data/lib/rails/schema/configuration.rb +2 -1
- data/lib/rails/schema/extractor/association_reader.rb +15 -4
- data/lib/rails/schema/extractor/column_reader.rb +2 -1
- data/lib/rails/schema/extractor/structure_sql_parser.rb +131 -0
- data/lib/rails/schema/version.rb +1 -1
- data/lib/rails/schema.rb +16 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97d15de972c2e7c9c3d9e395d0b610b95806e7f6ef5729eed214f5cb7723bb61
|
|
4
|
+
data.tar.gz: 7edc0d7d8cd246ad80cc2dd54936a0c9802b662476400f095d076bed651d6472
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ba7bf7dd2ac15538fa4a9f14b2772c4065fd6dc51a5325e1863e4d4e83838cdaa14497f5b4a4f6d7ffcdc014fe03ba9e1f3464e722ef57f73a56d14314997f0
|
|
7
|
+
data.tar.gz: f41572f08a43be003061f2e47914ed36ed27b2718b10d0d3e238c9ca436ea3230ca7e61e5213acdbf50cab6f19b96506d5d1c8d0de33bc2831e2ab84181adac0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.2] - 2026-02-22
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `StructureSqlParser` for extracting schema from `db/structure.sql` files
|
|
12
|
+
- `schema_format` configuration option (`:ruby`, `:sql`, `:auto`)
|
|
13
|
+
- `warn` messages to all silent rescue blocks in `AssociationReader` and `ColumnReader`
|
|
14
|
+
|
|
15
|
+
## [0.1.1] - 2026-02-17
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- ERD-style connections with crow's foot notation, directional indicators, and column-level attachment points
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Refactored edge routing with cubic Bezier curves and improved self-referential association handling
|
|
24
|
+
|
|
25
|
+
## [0.1.0] - 2026-02-15
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Initial release
|
|
30
|
+
- Interactive HTML visualization of Rails database schema (force-directed ERD)
|
|
31
|
+
- Model introspection: associations, columns, and schema file parsing
|
|
32
|
+
- Self-contained single HTML file output (no external dependencies)
|
|
33
|
+
- Searchable sidebar, click-to-focus, dark/light theme, keyboard shortcuts
|
|
34
|
+
- Rake task (`rails_schema:generate`) and programmatic API
|
|
35
|
+
- Configuration DSL: output path, title, theme, expand columns, exclude models
|
data/PROJECT.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**Name:** `rails-schema`
|
|
10
10
|
**Module:** `Rails::Schema`
|
|
11
|
-
**Version:** `0.1.
|
|
11
|
+
**Version:** `0.1.2`
|
|
12
12
|
|
|
13
13
|
Rails::Schema introspects a Rails app's models, associations, and database columns at runtime, then generates a single self-contained HTML file with an interactive, explorable entity-relationship diagram. No external server, no SaaS dependency — just one command and a browser.
|
|
14
14
|
|
|
@@ -43,7 +43,7 @@ Rails::Schema.generate(output: "docs/schema.html")
|
|
|
43
43
|
|
|
44
44
|
| Layer | Responsibility | Key Classes |
|
|
45
45
|
|---|---|---|
|
|
46
|
-
| **Extractor** | Introspects Rails environment; collects models, columns, associations | `Rails::Schema::Extractor::ModelScanner`, `ColumnReader`, `AssociationReader`, `SchemaFileParser` |
|
|
46
|
+
| **Extractor** | Introspects Rails environment; collects models, columns, associations | `Rails::Schema::Extractor::ModelScanner`, `ColumnReader`, `AssociationReader`, `SchemaFileParser`, `StructureSqlParser` |
|
|
47
47
|
| **Transformer** | Normalizes extracted data into a serializable graph structure (nodes + edges + metadata) | `Rails::Schema::Transformer::GraphBuilder`, `Node`, `Edge` |
|
|
48
48
|
| **Renderer** | Takes the graph data and injects it into an HTML/JS/CSS template using ERB | `Rails::Schema::Renderer::HtmlGenerator` |
|
|
49
49
|
| **Railtie** | Provides the `rails_schema:generate` rake task | `Rails::Schema::Railtie` |
|
|
@@ -52,13 +52,23 @@ Rails::Schema.generate(output: "docs/schema.html")
|
|
|
52
52
|
|
|
53
53
|
```ruby
|
|
54
54
|
def generate(output: nil)
|
|
55
|
-
schema_data =
|
|
55
|
+
schema_data = parse_schema
|
|
56
56
|
models = Extractor::ModelScanner.new(schema_data: schema_data).scan
|
|
57
57
|
column_reader = Extractor::ColumnReader.new(schema_data: schema_data)
|
|
58
58
|
graph_data = Transformer::GraphBuilder.new(column_reader: column_reader).build(models)
|
|
59
59
|
generator = Renderer::HtmlGenerator.new(graph_data: graph_data)
|
|
60
60
|
generator.render_to_file(output)
|
|
61
61
|
end
|
|
62
|
+
|
|
63
|
+
def parse_schema
|
|
64
|
+
case configuration.schema_format
|
|
65
|
+
when :ruby then Extractor::SchemaFileParser.new.parse
|
|
66
|
+
when :sql then Extractor::StructureSqlParser.new.parse
|
|
67
|
+
when :auto
|
|
68
|
+
data = Extractor::SchemaFileParser.new.parse
|
|
69
|
+
data.empty? ? Extractor::StructureSqlParser.new.parse : data
|
|
70
|
+
end
|
|
71
|
+
end
|
|
62
72
|
```
|
|
63
73
|
|
|
64
74
|
---
|
|
@@ -68,8 +78,9 @@ end
|
|
|
68
78
|
### 3.1 Sources of Truth
|
|
69
79
|
|
|
70
80
|
1. **`db/schema.rb` parsing** — `SchemaFileParser` parses the schema file line-by-line with regex to extract table names, column definitions (name, type, nullable, default), and primary key info. This is attempted first and used as a fast, database-free source.
|
|
71
|
-
2.
|
|
72
|
-
3.
|
|
81
|
+
2. **`db/structure.sql` parsing** — `StructureSqlParser` parses SQL `CREATE TABLE` statements for projects using `config.active_record.schema_format = :sql`. Maps SQL types to Rails-friendly types, detects `NOT NULL`, `DEFAULT` values, and primary keys. Handles schema-qualified names (`public.users`), timestamp precision (`timestamp(6)`), and both quoted and unquoted identifiers.
|
|
82
|
+
3. **ActiveRecord reflection API** — `AssociationReader` uses `Model.reflect_on_all_associations` for associations (`has_many`, `belongs_to`, `has_one`, `has_and_belongs_to_many`), including `:through` and `:polymorphic`.
|
|
83
|
+
4. **`Model.columns`** — `ColumnReader` falls back to `model.columns` via ActiveRecord when a table is not found in schema_data.
|
|
73
84
|
|
|
74
85
|
### 3.2 Model Discovery
|
|
75
86
|
|
|
@@ -92,7 +103,19 @@ When `schema_data` is available, table existence is checked against parsed schem
|
|
|
92
103
|
- Handles custom primary key types (`id: :uuid`, `id: :bigint`) and `id: false`
|
|
93
104
|
- Skips index definitions
|
|
94
105
|
|
|
95
|
-
### 3.4
|
|
106
|
+
### 3.4 Structure SQL Parser
|
|
107
|
+
|
|
108
|
+
`StructureSqlParser` provides database-free column extraction from SQL dumps:
|
|
109
|
+
|
|
110
|
+
- Parses `CREATE TABLE` statements from `db/structure.sql`
|
|
111
|
+
- Maps SQL types to Rails types (e.g. `character varying` → `string`, `bigint` → `bigint`, `timestamp without time zone` → `datetime`)
|
|
112
|
+
- Handles schema-qualified table names (`public.users` → `users`)
|
|
113
|
+
- Handles timestamp precision (`timestamp(6) without time zone`)
|
|
114
|
+
- Detects primary keys from `CONSTRAINT ... PRIMARY KEY` and inline `PRIMARY KEY`
|
|
115
|
+
- Extracts `NOT NULL`, `DEFAULT` values (strings, numbers, booleans)
|
|
116
|
+
- Skips constraint lines (`CONSTRAINT`, `UNIQUE`, `CHECK`, `FOREIGN KEY`, etc.)
|
|
117
|
+
|
|
118
|
+
### 3.5 Intermediate Data Format (JSON Graph)
|
|
96
119
|
|
|
97
120
|
```json
|
|
98
121
|
{
|
|
@@ -195,6 +218,7 @@ Rails::Schema.configure do |config|
|
|
|
195
218
|
config.title = "Database Schema" # Page title
|
|
196
219
|
config.theme = :auto # :light, :dark, :auto
|
|
197
220
|
config.expand_columns = false # Start with columns expanded
|
|
221
|
+
config.schema_format = :auto # :auto, :ruby, or :sql
|
|
198
222
|
end
|
|
199
223
|
```
|
|
200
224
|
|
|
@@ -207,14 +231,15 @@ rails-schema/
|
|
|
207
231
|
├── lib/
|
|
208
232
|
│ ├── rails/schema.rb # Entry point, configuration DSL, generate method
|
|
209
233
|
│ └── rails/schema/
|
|
210
|
-
│ ├── version.rb # VERSION = "0.1.
|
|
211
|
-
│ ├── configuration.rb # Config object (
|
|
234
|
+
│ ├── version.rb # VERSION = "0.1.2"
|
|
235
|
+
│ ├── configuration.rb # Config object (6 attributes)
|
|
212
236
|
│ ├── railtie.rb # Rails integration, rake task
|
|
213
237
|
│ ├── extractor/
|
|
214
238
|
│ │ ├── model_scanner.rb # Discovers AR models
|
|
215
239
|
│ │ ├── association_reader.rb # Reads reflections
|
|
216
240
|
│ │ ├── column_reader.rb # Reads columns (schema_data or AR)
|
|
217
|
-
│ │
|
|
241
|
+
│ │ ├── schema_file_parser.rb # Parses db/schema.rb
|
|
242
|
+
│ │ └── structure_sql_parser.rb # Parses db/structure.sql
|
|
218
243
|
│ ├── transformer/
|
|
219
244
|
│ │ ├── graph_builder.rb # Builds node/edge graph
|
|
220
245
|
│ │ ├── node.rb # Value object
|
|
@@ -238,7 +263,8 @@ rails-schema/
|
|
|
238
263
|
│ │ ├── model_scanner_spec.rb
|
|
239
264
|
│ │ ├── column_reader_spec.rb
|
|
240
265
|
│ │ ├── association_reader_spec.rb
|
|
241
|
-
│ │
|
|
266
|
+
│ │ ├── schema_file_parser_spec.rb
|
|
267
|
+
│ │ └── structure_sql_parser_spec.rb
|
|
242
268
|
│ ├── transformer/
|
|
243
269
|
│ │ └── graph_builder_spec.rb
|
|
244
270
|
│ └── renderer/
|
|
@@ -263,9 +289,9 @@ rails-schema/
|
|
|
263
289
|
|
|
264
290
|
A mounted engine requires a running server. A static file can be generated in CI, committed to the repo, and opened by anyone — including non-developers looking at a data model.
|
|
265
291
|
|
|
266
|
-
### Why parse schema.rb?
|
|
292
|
+
### Why parse schema.rb / structure.sql?
|
|
267
293
|
|
|
268
|
-
Parsing `db/schema.rb` allows column extraction without a database connection. This means the gem can work in CI environments or development setups where the database isn't running. It also avoids eager-loading the entire app just to read column metadata.
|
|
294
|
+
Parsing `db/schema.rb` or `db/structure.sql` allows column extraction without a database connection. This means the gem can work in CI environments or development setups where the database isn't running. It also avoids eager-loading the entire app just to read column metadata. The `schema_format: :auto` default tries `schema.rb` first, then falls back to `structure.sql`, so the gem works out of the box regardless of which format a project uses.
|
|
269
295
|
|
|
270
296
|
### Why force-directed layout?
|
|
271
297
|
|
|
@@ -292,12 +318,12 @@ spec.add_dependency "railties", ">= 6.0"
|
|
|
292
318
|
|
|
293
319
|
| Layer | Approach |
|
|
294
320
|
|---|---|
|
|
295
|
-
| Extractor | Unit tests with in-memory SQLite models (User, Post, Comment, Tag) |
|
|
321
|
+
| Extractor | Unit tests with in-memory SQLite models (User, Post, Comment, Tag); rescue-path warnings tested via `output(...).to_stderr` |
|
|
296
322
|
| Transformer | Pure Ruby unit tests — graph building, edge filtering |
|
|
297
323
|
| Renderer | Output tests — verify HTML structure, embedded data, script injection safety |
|
|
298
324
|
| Configuration | Unit tests for defaults and attribute setting |
|
|
299
325
|
|
|
300
|
-
**
|
|
326
|
+
**108 tests, all passing.** Run with `bundle exec rspec`.
|
|
301
327
|
|
|
302
328
|
---
|
|
303
329
|
|
|
@@ -319,4 +345,4 @@ spec.add_dependency "railties", ">= 6.0"
|
|
|
319
345
|
|
|
320
346
|
---
|
|
321
347
|
|
|
322
|
-
*Document reflects the current implementation (v0.1.
|
|
348
|
+
*Document reflects the current implementation (v0.1.2). Future enhancements are aspirational and subject to refinement.*
|
data/README.md
CHANGED
|
@@ -48,6 +48,7 @@ Rails::Schema.configure do |config|
|
|
|
48
48
|
config.title = "My App Schema"
|
|
49
49
|
config.theme = :auto # :auto, :light, or :dark
|
|
50
50
|
config.expand_columns = false # start with columns collapsed
|
|
51
|
+
config.schema_format = :auto # :auto, :ruby, or :sql
|
|
51
52
|
config.exclude_models = [
|
|
52
53
|
"ActiveStorage::Blob",
|
|
53
54
|
"ActiveStorage::Attachment",
|
|
@@ -56,13 +57,32 @@ Rails::Schema.configure do |config|
|
|
|
56
57
|
end
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
| Option | Default | Description |
|
|
61
|
+
|--------|---------|-------------|
|
|
62
|
+
| `output_path` | `"docs/schema.html"` | Path for the generated HTML file |
|
|
63
|
+
| `title` | `"Database Schema"` | Title shown in the HTML page |
|
|
64
|
+
| `theme` | `:auto` | Color theme — `:auto`, `:light`, or `:dark` |
|
|
65
|
+
| `expand_columns` | `false` | Whether model nodes start with columns expanded |
|
|
66
|
+
| `schema_format` | `:auto` | Schema source — `:auto`, `:ruby`, or `:sql` (see below) |
|
|
67
|
+
| `exclude_models` | `[]` | Models to hide; supports exact names and wildcard prefixes (`"ActionMailbox::*"`) |
|
|
68
|
+
|
|
69
|
+
### Schema format
|
|
70
|
+
|
|
71
|
+
Rails projects can use either `db/schema.rb` (Ruby DSL) or `db/structure.sql` (raw SQL dump) to represent the database schema. Set `config.active_record.schema_format = :sql` in your Rails app to use `structure.sql`.
|
|
72
|
+
|
|
73
|
+
| Value | Behavior |
|
|
74
|
+
|-------|----------|
|
|
75
|
+
| `:auto` | Tries `db/schema.rb` first, falls back to `db/structure.sql` |
|
|
76
|
+
| `:ruby` | Only reads `db/schema.rb` |
|
|
77
|
+
| `:sql` | Only reads `db/structure.sql` |
|
|
78
|
+
|
|
59
79
|
## How it works
|
|
60
80
|
|
|
61
|
-
The gem parses your `db/schema.rb` file to extract table and column information — **no database connection required**. It also introspects loaded ActiveRecord models for association metadata. This means the gem works even if you don't have a local database set up, as long as
|
|
81
|
+
The gem parses your `db/schema.rb` or `db/structure.sql` file to extract table and column information — **no database connection required**. It also introspects loaded ActiveRecord models for association metadata. This means the gem works even if you don't have a local database set up, as long as a schema file is present (which is standard in Rails projects under version control).
|
|
62
82
|
|
|
63
83
|
## Features
|
|
64
84
|
|
|
65
|
-
- **No database required** — reads
|
|
85
|
+
- **No database required** — reads from `db/schema.rb` or `db/structure.sql`
|
|
66
86
|
- **Force-directed layout** — models cluster naturally by association density
|
|
67
87
|
- **Searchable sidebar** — filter models by name or table
|
|
68
88
|
- **Click-to-focus** — click a model to highlight its neighborhood, fading unrelated models
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Rails
|
|
4
4
|
module Schema
|
|
5
5
|
class Configuration
|
|
6
|
-
attr_accessor :output_path, :exclude_models, :title, :theme, :expand_columns
|
|
6
|
+
attr_accessor :output_path, :exclude_models, :title, :theme, :expand_columns, :schema_format
|
|
7
7
|
|
|
8
8
|
def initialize
|
|
9
9
|
@output_path = "docs/schema.html"
|
|
@@ -11,6 +11,7 @@ module Rails
|
|
|
11
11
|
@title = "Database Schema"
|
|
12
12
|
@theme = :auto
|
|
13
13
|
@expand_columns = false
|
|
14
|
+
@schema_format = :auto
|
|
14
15
|
end
|
|
15
16
|
end
|
|
16
17
|
end
|
|
@@ -26,16 +26,27 @@ module Rails
|
|
|
26
26
|
association_type: ref.macro.to_s,
|
|
27
27
|
label: ref.name.to_s,
|
|
28
28
|
foreign_key: ref.foreign_key.to_s,
|
|
29
|
-
through: ref
|
|
30
|
-
polymorphic: ref
|
|
29
|
+
through: through_name(ref),
|
|
30
|
+
polymorphic: polymorphic?(ref)
|
|
31
31
|
}
|
|
32
|
-
rescue StandardError
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
warn "[rails-schema] Could not read association #{ref.name} on #{model.name}: #{e.class}: #{e.message}"
|
|
33
34
|
nil
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
def through_name(ref)
|
|
38
|
+
ref.options[:through]&.to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def polymorphic?(ref)
|
|
42
|
+
ref.options[:as] ? true : false
|
|
43
|
+
end
|
|
44
|
+
|
|
36
45
|
def target_model_name(ref)
|
|
37
46
|
ref.klass.name
|
|
38
|
-
rescue StandardError
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
warn "[rails-schema] Could not resolve target for #{ref.name}, " \
|
|
49
|
+
"falling back to #{ref.class_name}: #{e.class}: #{e.message}"
|
|
39
50
|
ref.class_name
|
|
40
51
|
end
|
|
41
52
|
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Schema
|
|
5
|
+
module Extractor
|
|
6
|
+
class StructureSqlParser
|
|
7
|
+
SQL_TYPE_MAP = {
|
|
8
|
+
"character varying" => "string", "varchar" => "string",
|
|
9
|
+
"integer" => "integer", "smallint" => "integer", "serial" => "integer",
|
|
10
|
+
"bigint" => "bigint", "bigserial" => "bigint",
|
|
11
|
+
"boolean" => "boolean", "text" => "text",
|
|
12
|
+
"timestamp without time zone" => "datetime", "timestamp with time zone" => "datetime",
|
|
13
|
+
"timestamp" => "datetime",
|
|
14
|
+
"json" => "json", "jsonb" => "jsonb", "uuid" => "uuid",
|
|
15
|
+
"numeric" => "decimal", "decimal" => "decimal", "money" => "decimal",
|
|
16
|
+
"date" => "date",
|
|
17
|
+
"float" => "float", "double precision" => "float", "real" => "float",
|
|
18
|
+
"bytea" => "binary"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
COMPOUND_TYPE_RE = /\A(character\s+varying|bit\s+varying|double\s+precision|
|
|
22
|
+
timestamp(?:\(\d+\))?\s+with(?:out)?\s+time\s+zone)/ix
|
|
23
|
+
CONSTRAINT_RE = /\A(CONSTRAINT|UNIQUE|CHECK|EXCLUDE|FOREIGN\s+KEY)\b/i
|
|
24
|
+
PK_CONSTRAINT_RE = /PRIMARY\s+KEY\s*\(([^)]+)\)/i
|
|
25
|
+
|
|
26
|
+
def initialize(structure_path = nil)
|
|
27
|
+
@structure_path = structure_path
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse
|
|
31
|
+
path = resolve_path
|
|
32
|
+
return {} unless path && File.exist?(path)
|
|
33
|
+
|
|
34
|
+
parse_content(File.read(path))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse_content(content)
|
|
38
|
+
tables = {}
|
|
39
|
+
|
|
40
|
+
content.scan(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([\w."]+)\s*\((.*?)\)\s*;/mi) do |table_name, body|
|
|
41
|
+
name = extract_table_name(table_name)
|
|
42
|
+
columns, pk_columns = parse_table_body(body)
|
|
43
|
+
pk_columns.each { |pk| columns.find { |c| c[:name] == pk }&.[]= :primary, true }
|
|
44
|
+
tables[name] = columns
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
tables
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def resolve_path
|
|
53
|
+
return @structure_path if @structure_path
|
|
54
|
+
return ::Rails.root.join("db", "structure.sql").to_s if defined?(::Rails.root) && ::Rails.root
|
|
55
|
+
|
|
56
|
+
File.join(Dir.pwd, "db", "structure.sql")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def unquote(identifier) = identifier.delete('"')
|
|
60
|
+
|
|
61
|
+
def extract_table_name(raw)
|
|
62
|
+
unquote(raw).split(".").last
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse_table_body(body)
|
|
66
|
+
columns = []
|
|
67
|
+
pk_columns = []
|
|
68
|
+
body.each_line do |raw|
|
|
69
|
+
line = raw.strip.chomp(",")
|
|
70
|
+
next if line.empty?
|
|
71
|
+
|
|
72
|
+
if (pk = extract_pk_constraint(line))
|
|
73
|
+
pk_columns.concat(pk)
|
|
74
|
+
elsif !line.match?(CONSTRAINT_RE) && (col = parse_column_line(line))
|
|
75
|
+
pk_columns << col[:name] if col.delete(:inline_pk)
|
|
76
|
+
columns << col
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
[columns, pk_columns]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def extract_pk_constraint(line)
|
|
83
|
+
return unless (match = line.match(PK_CONSTRAINT_RE))
|
|
84
|
+
|
|
85
|
+
match[1].split(",").map { |c| unquote(c.strip) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def parse_column_line(line)
|
|
89
|
+
match = line.match(/\A("?\w+"?)\s+(.+)/i)
|
|
90
|
+
return nil unless match
|
|
91
|
+
|
|
92
|
+
rest = match[2]
|
|
93
|
+
type = extract_type(rest)
|
|
94
|
+
return nil unless type
|
|
95
|
+
|
|
96
|
+
build_column(unquote(match[1]), rest, type)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def build_column(col_name, rest, type)
|
|
100
|
+
{
|
|
101
|
+
name: col_name,
|
|
102
|
+
type: SQL_TYPE_MAP.fetch(type, type),
|
|
103
|
+
nullable: !rest.match?(/\bNOT\s+NULL\b/i),
|
|
104
|
+
default: extract_default(rest),
|
|
105
|
+
primary: false,
|
|
106
|
+
inline_pk: rest.match?(/\bPRIMARY\s+KEY\b/i)
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def extract_type(rest)
|
|
111
|
+
if (m = rest.match(COMPOUND_TYPE_RE))
|
|
112
|
+
m[1].downcase.gsub(/\(\d+\)/, "")
|
|
113
|
+
elsif rest.match?(/\A(FOREIGN\s+KEY)\b/i)
|
|
114
|
+
nil
|
|
115
|
+
else
|
|
116
|
+
rest[/\A(\w+)/i, 1]&.downcase
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def extract_default(rest)
|
|
121
|
+
case rest
|
|
122
|
+
when /\bDEFAULT\s+'([^']*)'(?:::\w+)?/i, /\bDEFAULT\s+(\d+(?:\.\d+)?)\b/i
|
|
123
|
+
Regexp.last_match(1)
|
|
124
|
+
when /\bDEFAULT\s+true\b/i then "true"
|
|
125
|
+
when /\bDEFAULT\s+false\b/i then "false"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/rails/schema/version.rb
CHANGED
data/lib/rails/schema.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "schema/configuration"
|
|
|
5
5
|
require_relative "schema/transformer/node"
|
|
6
6
|
require_relative "schema/transformer/edge"
|
|
7
7
|
require_relative "schema/extractor/schema_file_parser"
|
|
8
|
+
require_relative "schema/extractor/structure_sql_parser"
|
|
8
9
|
require_relative "schema/extractor/model_scanner"
|
|
9
10
|
require_relative "schema/extractor/column_reader"
|
|
10
11
|
require_relative "schema/extractor/association_reader"
|
|
@@ -29,13 +30,27 @@ module Rails
|
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def generate(output: nil)
|
|
32
|
-
schema_data =
|
|
33
|
+
schema_data = parse_schema
|
|
33
34
|
models = Extractor::ModelScanner.new(schema_data: schema_data).scan
|
|
34
35
|
column_reader = Extractor::ColumnReader.new(schema_data: schema_data)
|
|
35
36
|
graph_data = Transformer::GraphBuilder.new(column_reader: column_reader).build(models)
|
|
36
37
|
generator = Renderer::HtmlGenerator.new(graph_data: graph_data)
|
|
37
38
|
generator.render_to_file(output)
|
|
38
39
|
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def parse_schema
|
|
44
|
+
case configuration.schema_format
|
|
45
|
+
when :ruby
|
|
46
|
+
Extractor::SchemaFileParser.new.parse
|
|
47
|
+
when :sql
|
|
48
|
+
Extractor::StructureSqlParser.new.parse
|
|
49
|
+
when :auto
|
|
50
|
+
data = Extractor::SchemaFileParser.new.parse
|
|
51
|
+
data.empty? ? Extractor::StructureSqlParser.new.parse : data
|
|
52
|
+
end
|
|
53
|
+
end
|
|
39
54
|
end
|
|
40
55
|
end
|
|
41
56
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-schema
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Kislichenko
|
|
@@ -45,6 +45,7 @@ executables: []
|
|
|
45
45
|
extensions: []
|
|
46
46
|
extra_rdoc_files: []
|
|
47
47
|
files:
|
|
48
|
+
- CHANGELOG.md
|
|
48
49
|
- LICENSE.txt
|
|
49
50
|
- PROJECT.md
|
|
50
51
|
- README.md
|
|
@@ -61,6 +62,7 @@ files:
|
|
|
61
62
|
- lib/rails/schema/extractor/column_reader.rb
|
|
62
63
|
- lib/rails/schema/extractor/model_scanner.rb
|
|
63
64
|
- lib/rails/schema/extractor/schema_file_parser.rb
|
|
65
|
+
- lib/rails/schema/extractor/structure_sql_parser.rb
|
|
64
66
|
- lib/rails/schema/railtie.rb
|
|
65
67
|
- lib/rails/schema/renderer/html_generator.rb
|
|
66
68
|
- lib/rails/schema/transformer/edge.rb
|