schema_generator 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +41 -0
- data/schema_generator.rb +312 -0
- data/templates/schema.mysql.sql +8 -0
- data/templates/schema.postgresql.sql +8 -0
- data/templates/schema.sqlite.sql +8 -0
- metadata +45 -0
data/README
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
Schema Generator v0.1.0
|
2
|
+
by Scott Laird, scott@sigkill.org
|
3
|
+
http://scottstuff.net
|
4
|
+
|
5
|
+
This is a SQL schema generator for Rails. It slurps up all of the migration files in db/migrate and turns them into a single SQL schema file for each supported database type. The schema files contains table definitions, indexes, and any seed data created by Model.create() calls in the migrations.
|
6
|
+
|
7
|
+
Supported databases:
|
8
|
+
* MySQL
|
9
|
+
* PostgreSQL
|
10
|
+
* SQLite
|
11
|
+
|
12
|
+
Usage
|
13
|
+
-----
|
14
|
+
|
15
|
+
To use, run './scripts/generate schema' on any Rails project. This will produce three files:
|
16
|
+
|
17
|
+
* db/schema.mysql.sql
|
18
|
+
* db/schema.postgresql.sql
|
19
|
+
* db/schema.sqlite.sql
|
20
|
+
|
21
|
+
If these files already exist, then Rails will prompt you before overwriting them.
|
22
|
+
|
23
|
+
If your Rails project already has migrations to build its entire schema from scratch, then this will work right off the bat. If not, you may wish to create a db/migrate/0_initial_schema.rb migration and use it to create your initial schema using the usual migration create_table methods.
|
24
|
+
|
25
|
+
You can see an example in Typo (http://typo.leetsoft.com). This generator was written to reduce the pain involved in creating new migrations for Typo, because each migration required an equivalent change to 3 different hand-edited static schema files, which was usually an error-prone process, because no one user tests all three DB types. Now we can just use './script/generate schema' and be done with it.
|
26
|
+
|
27
|
+
|
28
|
+
Supported Migration Actions
|
29
|
+
---------------------------
|
30
|
+
|
31
|
+
This generator currently supports create_table, add_column, drop_column, add_index, drop_index, and *.create calls. Any model find calls will immediately return an empty array; this allows data migration blocks in migration files to be safely ignored.
|
32
|
+
|
33
|
+
Column change operations aren't currently supported, but that's simply because I don't have any good test cases. The code needed is probably only 5 lines long.
|
34
|
+
|
35
|
+
|
36
|
+
Debugging and Troubleshooting
|
37
|
+
-----------------------------
|
38
|
+
|
39
|
+
This generator reaches deep into the guts of Rails; it has to make several changes to the DB drivers and ActiveRecord::Base in order to work. This means that it may have issues with future Rails releases. It has been tested with 0.13.1. All of these changes are made on the fly using open classes, so installing this generator will not break any outside code--the modifications are only active while the generator is running.
|
40
|
+
|
41
|
+
If you have any other problems, please mail me at scott@sigkill.org or post a comment on http://scottstuff.net/. If there is enough interest, then I'll start a schema generator mailing list.
|
data/schema_generator.rb
ADDED
@@ -0,0 +1,312 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
class SchemaGenerator < Rails::Generator::Base
|
4
|
+
def initialize(*runtime_args)
|
5
|
+
super(*runtime_args)
|
6
|
+
|
7
|
+
migration_classes = Array.new
|
8
|
+
|
9
|
+
migration_0 = load_migration(0)
|
10
|
+
if(migration_0)
|
11
|
+
migration_classes.push migration_0
|
12
|
+
end
|
13
|
+
|
14
|
+
migration_number = 1
|
15
|
+
migration = true
|
16
|
+
|
17
|
+
while migration do
|
18
|
+
migration = load_migration(migration_number)
|
19
|
+
if(migration)
|
20
|
+
migration_classes.push migration
|
21
|
+
DBMigrator::Database.version = migration_number
|
22
|
+
end
|
23
|
+
migration_number += 1
|
24
|
+
end
|
25
|
+
|
26
|
+
STDERR.puts "Found #{migration_classes.size} migration classes"
|
27
|
+
DBMigrator::Database.disable_db
|
28
|
+
|
29
|
+
migration_classes.each do |mclass|
|
30
|
+
STDERR.puts "Starting migration for #{mclass}"
|
31
|
+
mclass.up
|
32
|
+
end
|
33
|
+
|
34
|
+
STDERR.puts "Migrations complete."
|
35
|
+
STDERR.puts " Tables found: #{DBMigrator::Database.tables.size}"
|
36
|
+
STDERR.puts " Indexes found: #{DBMigrator::Database.indexes.size}"
|
37
|
+
STDERR.puts " Records found: #{DBMigrator::Database.data.size}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def load_migration(migration_number)
|
41
|
+
globname = "db/migrate/#{migration_number}_*.rb"
|
42
|
+
file = Pathname.glob(globname).to_a.first.to_s
|
43
|
+
return nil unless file =~/\.rb$/
|
44
|
+
|
45
|
+
begin
|
46
|
+
file =~ %r{^(db/migrate/#{migration_number}_(.*)).rb$}
|
47
|
+
include_name = $1.to_s
|
48
|
+
class_base_name = $2.camelize
|
49
|
+
|
50
|
+
require include_name
|
51
|
+
return class_base_name.constantize
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def manifest
|
56
|
+
record do |m|
|
57
|
+
m.directory File.join('db')
|
58
|
+
|
59
|
+
m.template 'schema.postgresql.sql', File.join('db', "schema.postgresql.sql")
|
60
|
+
m.template 'schema.mysql.sql', File.join('db', "schema.mysql.sql")
|
61
|
+
m.template 'schema.sqlite.sql', File.join('db', "schema.sqlite.sql")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module ActiveRecord
|
67
|
+
class Base
|
68
|
+
alias_method :old_save, :save
|
69
|
+
|
70
|
+
def dbmigrate_save
|
71
|
+
end
|
72
|
+
|
73
|
+
class << self
|
74
|
+
alias_method :old_find, :find
|
75
|
+
alias_method :old_create, :create
|
76
|
+
|
77
|
+
def dbmigrate_find(*args)
|
78
|
+
[]
|
79
|
+
end
|
80
|
+
|
81
|
+
def dbmigrate_create(attributes = nil)
|
82
|
+
DBMigrator::Database.new_data(self,attributes)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class Migration
|
89
|
+
def self.create_table(name,options={},&block)
|
90
|
+
if(DBMigrator::Database.tables.find {|t| t.name == name})
|
91
|
+
raise "Duplicate table definition on #{name}"
|
92
|
+
end
|
93
|
+
table = DBMigrator::Table.new(name)
|
94
|
+
table.options = options
|
95
|
+
DBMigrator::Database.tables.push(table)
|
96
|
+
block.call(table)
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.drop_table(name)
|
100
|
+
DBMigrator::Database.tables.delete_if {|t| t.name == name}
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.add_column(tablename,name,type,options={})
|
104
|
+
table=DBMigrator::Database.tables.find{|t| t.name == tablename}
|
105
|
+
if(table.fields.find{|f| f.name == name})
|
106
|
+
raise "Duplicate field definition for #{tablename}.#{name}"
|
107
|
+
end
|
108
|
+
table.column(name,type,options)
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.remove_column(tablename,name)
|
112
|
+
table = DBMigrator::Database.tables.find{|t| t.name == tablename}
|
113
|
+
field = table.fields.delete_if {|f| f.name == name}
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.add_index(tablename,name,options={})
|
117
|
+
if(DBMigrator::Database.indexes.find {|i| i.table == tablename and i.name == name})
|
118
|
+
return # duplicate
|
119
|
+
end
|
120
|
+
|
121
|
+
index = DBMigrator::Index.new
|
122
|
+
index.table = tablename
|
123
|
+
index.name = name
|
124
|
+
DBMigrator::Database.indexes.push index
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.remove_index(tablename,name)
|
128
|
+
DBMigrator::Database.indexes.delete_if {|i| i.table == tablename and i.name == name}
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
module DBMigrator
|
134
|
+
class Table
|
135
|
+
attr_accessor :name, :options, :fields
|
136
|
+
|
137
|
+
def initialize(name)
|
138
|
+
@name = name
|
139
|
+
@fields = Array.new
|
140
|
+
end
|
141
|
+
|
142
|
+
def column(name,type,options={})
|
143
|
+
f = Field.new
|
144
|
+
f.name = name
|
145
|
+
f.type = type
|
146
|
+
f.options = options
|
147
|
+
@fields.push(f)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
class Index
|
152
|
+
attr_accessor :table, :name
|
153
|
+
end
|
154
|
+
|
155
|
+
class Field
|
156
|
+
attr_accessor :table, :name, :type, :options
|
157
|
+
end
|
158
|
+
|
159
|
+
class Data
|
160
|
+
attr_accessor :table, :data
|
161
|
+
|
162
|
+
def initialize(tablename, data)
|
163
|
+
@table=tablename
|
164
|
+
@data=data
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
class Database
|
169
|
+
cattr_accessor :tables, :indexes, :data, :version
|
170
|
+
|
171
|
+
@@tables = Array.new
|
172
|
+
@@indexes = Array.new
|
173
|
+
@@data = Array.new
|
174
|
+
@@version = 1
|
175
|
+
|
176
|
+
def self.new_data(tablename,data)
|
177
|
+
@@data.push(Data.new(tablename,data))
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.execute(sql,name)
|
181
|
+
if(sql =~ /create table/i)
|
182
|
+
@@commandstring += reformat_create_table(sql)
|
183
|
+
else
|
184
|
+
@@commandstring += sql
|
185
|
+
@@commandstring += ";\n"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.dbdriver
|
190
|
+
@@dbdriver
|
191
|
+
end
|
192
|
+
|
193
|
+
def self.enable_db
|
194
|
+
class << ActiveRecord::Base
|
195
|
+
alias find old_find
|
196
|
+
alias create old_create
|
197
|
+
end
|
198
|
+
|
199
|
+
ActiveRecord::Base.class_eval 'alias save old_save'
|
200
|
+
end
|
201
|
+
|
202
|
+
def self.disable_db
|
203
|
+
class << ActiveRecord::Base
|
204
|
+
alias find dbmigrate_find
|
205
|
+
alias create dbmigrate_create
|
206
|
+
end
|
207
|
+
ActiveRecord::Base.class_eval 'alias save dbmigrate_save'
|
208
|
+
end
|
209
|
+
|
210
|
+
def self.dump(databasename)
|
211
|
+
@@commandstring=''
|
212
|
+
case databasename
|
213
|
+
when 'postgresql'
|
214
|
+
@@dbdriver = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.new(nil)
|
215
|
+
when 'mysql'
|
216
|
+
@@dbdriver = ActiveRecord::ConnectionAdapters::MysqlAdapter.new(nil,nil)
|
217
|
+
when 'sqlite'
|
218
|
+
@@dbdriver = ActiveRecord::ConnectionAdapters::SQLiteAdapter.new(nil)
|
219
|
+
else
|
220
|
+
raise 'Unknown driver'
|
221
|
+
end
|
222
|
+
|
223
|
+
class << @@dbdriver
|
224
|
+
def execute(sql,name = nil)
|
225
|
+
DBMigrator::Database.execute(sql,name)
|
226
|
+
end
|
227
|
+
|
228
|
+
def columns(tablename,name = nil)
|
229
|
+
table = DBMigrator::Database.tables.find {|t| t.name.to_s == tablename}
|
230
|
+
table.fields.collect {|f| ActiveRecord::ConnectionAdapters::Column.new(f.name.to_s, nil, f.type)}
|
231
|
+
end
|
232
|
+
|
233
|
+
def insert(sql, name = nil, pk = nil, id_value = nil)
|
234
|
+
execute(sql, name = nil)
|
235
|
+
end
|
236
|
+
|
237
|
+
def rollback_db_transaction; end
|
238
|
+
def begin_db_transaction; end
|
239
|
+
def commit_db_transaction; end
|
240
|
+
|
241
|
+
# This is for the schema code, so we don't have security to worry about
|
242
|
+
# This implementation might be attackable, but it won't generally fail
|
243
|
+
# on reasonable strings.
|
244
|
+
def quote_string(string)
|
245
|
+
string.gsub(/\'/,"''")
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
@@commandstring += "-- tables \n\n"
|
250
|
+
|
251
|
+
tables.sort_by {|t| t.name.to_s}.each do |table|
|
252
|
+
dbdriver.create_table table.name, table.options do |t|
|
253
|
+
table.fields.each do |f|
|
254
|
+
t.column f.name, f.type, f.options
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
@@commandstring += "\n-- indexes \n\n" if indexes.size > 0
|
260
|
+
|
261
|
+
indexes.sort_by {|t| t.table.to_s}.each do |index|
|
262
|
+
dbdriver.add_index index.table, index.name.to_s
|
263
|
+
end
|
264
|
+
|
265
|
+
class << ActiveRecord::Base
|
266
|
+
def retrieve_connection
|
267
|
+
DBMigrator::Database.dbdriver
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
enable_db
|
272
|
+
|
273
|
+
ActiveRecord::Base.class_eval do
|
274
|
+
def attributes_from_column_definition
|
275
|
+
tables = DBMigrator::Database.tables
|
276
|
+
table = tables.find {|t| t.name.to_s == self.class.table_name}
|
277
|
+
table.fields.inject({}) do |a,f|
|
278
|
+
a[f.name.to_s]=nil
|
279
|
+
a
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
@@commandstring += "\n-- data \n\n" if @@data.size > 0
|
285
|
+
|
286
|
+
@@data.each do |data|
|
287
|
+
data.table.create(data.data)
|
288
|
+
end
|
289
|
+
|
290
|
+
@@commandstring += "\n-- schema version meta-info \n\n"
|
291
|
+
|
292
|
+
dbdriver.create_table :schema_info, :id => false do |t|
|
293
|
+
t.column :version, :integer
|
294
|
+
end
|
295
|
+
|
296
|
+
dbdriver.execute "insert into schema_info (version) values (#{version})"
|
297
|
+
|
298
|
+
disable_db
|
299
|
+
@@commandstring
|
300
|
+
end
|
301
|
+
|
302
|
+
def self.reformat_create_table(sql)
|
303
|
+
sql.gsub(/^(create table [^(]+\()/i,"\\1\n ").
|
304
|
+
gsub(/, /,",\n ").
|
305
|
+
gsub(/\) (ENGINE.*)?$\Z/,"\n) \\1;\n\n").
|
306
|
+
gsub(/\) ;$/,');')
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
end
|
311
|
+
|
312
|
+
$schema_generator = true
|
@@ -0,0 +1,8 @@
|
|
1
|
+
-- This file is autogenerated by the Rail schema generator, using
|
2
|
+
-- the schema defined in db/migration/*.rb
|
3
|
+
--
|
4
|
+
-- Do not edit this file. Instead, add a new migration using
|
5
|
+
-- ./script/generate migration <name>, and then run
|
6
|
+
-- ./script/generate schema
|
7
|
+
|
8
|
+
<%= DBMigrator::Database.dump('mysql') %>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
-- This file is autogenerated by the Rail schema generator, using
|
2
|
+
-- the schema defined in db/migration/*.rb
|
3
|
+
--
|
4
|
+
-- Do not edit this file. Instead, add a new migration using
|
5
|
+
-- ./script/generate migration <name>, and then run
|
6
|
+
-- ./script/generate schema
|
7
|
+
|
8
|
+
<%= DBMigrator::Database.dump('postgresql') %>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
-- This file is autogenerated by the Rail schema generator, using
|
2
|
+
-- the schema defined in db/migration/*.rb
|
3
|
+
--
|
4
|
+
-- Do not edit this file. Instead, add a new migration using
|
5
|
+
-- ./script/generate migration <name>, and then run
|
6
|
+
-- ./script/generate schema
|
7
|
+
|
8
|
+
<%= DBMigrator::Database.dump('sqlite') %>
|
metadata
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.11
|
3
|
+
specification_version: 1
|
4
|
+
name: schema_generator
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.1.0
|
7
|
+
date: 2005-09-02 00:00:00 -07:00
|
8
|
+
summary: "The rails schema generator generates complete SQL schemas from a collection of
|
9
|
+
rails migrations. It currently produces one schema file each for MySQL,
|
10
|
+
PostgreSQL, and SQLite."
|
11
|
+
require_paths:
|
12
|
+
- lib
|
13
|
+
email: scott@sigkill.org
|
14
|
+
homepage: http://scottstuff.net
|
15
|
+
rubyforge_project:
|
16
|
+
description:
|
17
|
+
autorequire: schema_generator
|
18
|
+
default_executable:
|
19
|
+
bindir: bin
|
20
|
+
has_rdoc: false
|
21
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
22
|
+
requirements:
|
23
|
+
-
|
24
|
+
- ">"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.0.0
|
27
|
+
version:
|
28
|
+
platform: ruby
|
29
|
+
signing_key:
|
30
|
+
cert_chain:
|
31
|
+
authors:
|
32
|
+
- Scott Laird
|
33
|
+
files:
|
34
|
+
- templates/schema.mysql.sql
|
35
|
+
- templates/schema.postgresql.sql
|
36
|
+
- templates/schema.sqlite.sql
|
37
|
+
- schema_generator.rb
|
38
|
+
- README
|
39
|
+
test_files: []
|
40
|
+
rdoc_options: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
executables: []
|
43
|
+
extensions: []
|
44
|
+
requirements: []
|
45
|
+
dependencies: []
|