pgmodelgen 0.4.5
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/lib/pgmodelgen/railtie.rb +11 -0
- data/lib/pgmodelgen.rb +4 -0
- data/lib/tasks/pgmodelgen.rake +781 -0
- metadata +89 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013 Darwin
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
= pgmodelgen
|
2
|
+
|
3
|
+
Description goes here.
|
4
|
+
|
5
|
+
== Contributing to pgmodelgen
|
6
|
+
|
7
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
8
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
9
|
+
* Fork the project.
|
10
|
+
* Start a feature/bugfix branch.
|
11
|
+
* Commit and push until you are happy with your contribution.
|
12
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
13
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
14
|
+
|
15
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2013 Darwin. See LICENSE.txt for
|
18
|
+
further details.
|
19
|
+
|
data/lib/pgmodelgen.rb
ADDED
@@ -0,0 +1,781 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
# TODO Seperate parsing of postgresql meta data from model generating, the value of the
|
4
|
+
# project would be much more. Also it would open up to suporting other dbs.
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
# TODO Add timestamp syntax validation
|
9
|
+
# TODO Update so that a unique constraint on a foreign key changes the has_many to has_one
|
10
|
+
# TODO Add hint where a foregin key is to a many to many table
|
11
|
+
|
12
|
+
# DONE Add warning about missing index on foreign keys
|
13
|
+
# DONE Fix foreign_key mistake
|
14
|
+
# DONE Make the result a bit nicer
|
15
|
+
# DONE Add file writing code and code that creates the models
|
16
|
+
# DONE Change so that foreign key canstraint name defines the bindings
|
17
|
+
# (belongs_to:article),(has_a:video_url)
|
18
|
+
|
19
|
+
require 'active_support'
|
20
|
+
|
21
|
+
|
22
|
+
class PGGen
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
def initialize model_name,schema_name,dbname,regex = nil
|
27
|
+
|
28
|
+
puts "Generating for: #{dbname}.#{schema_name} with prefix #{model_name}"
|
29
|
+
|
30
|
+
if regex
|
31
|
+
@regex = Regexp.compile(regex)
|
32
|
+
else
|
33
|
+
@regex = Regexp.compile(".*")
|
34
|
+
end
|
35
|
+
@schema_name = schema_name;
|
36
|
+
|
37
|
+
require 'pg'
|
38
|
+
require 'FileUtils'
|
39
|
+
|
40
|
+
con = PGconn.open(:dbname => dbname)
|
41
|
+
con.exec "set search_path to #{@schema_name}"
|
42
|
+
|
43
|
+
@model_name = model_name.capitalize
|
44
|
+
allModelsContent = {}
|
45
|
+
|
46
|
+
#
|
47
|
+
# Get all the table names and oids
|
48
|
+
#
|
49
|
+
|
50
|
+
result = con.query "
|
51
|
+
select
|
52
|
+
|
53
|
+
pg_class.oid
|
54
|
+
,pg_class.relname
|
55
|
+
|
56
|
+
from pg_class
|
57
|
+
|
58
|
+
join pg_namespace on pg_namespace.oid = pg_class.relnamespace
|
59
|
+
|
60
|
+
where
|
61
|
+
|
62
|
+
(relkind = 'r' or relkind = 'v')
|
63
|
+
and pg_namespace.nspname = '#{schema_name}'
|
64
|
+
"
|
65
|
+
|
66
|
+
|
67
|
+
#
|
68
|
+
# Print matching tables
|
69
|
+
#
|
70
|
+
the_tables = []
|
71
|
+
puts "\n\tCreating models for these tables\n\n"
|
72
|
+
result.each do |row|
|
73
|
+
|
74
|
+
if @regex.match(row['relname'])
|
75
|
+
puts "\t\t#{row['relname']}"
|
76
|
+
the_tables.push row['relname']
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
|
83
|
+
#
|
84
|
+
# Get dependancy tables
|
85
|
+
#
|
86
|
+
deps = {}
|
87
|
+
result.each do |row|
|
88
|
+
|
89
|
+
if @regex.match(row['relname'])
|
90
|
+
|
91
|
+
table_oid = row['oid']
|
92
|
+
|
93
|
+
con.query("
|
94
|
+
|
95
|
+
|
96
|
+
select * from
|
97
|
+
|
98
|
+
(
|
99
|
+
|
100
|
+
select
|
101
|
+
|
102
|
+
conname
|
103
|
+
,case /* c = check constraint, f = foreign key constraint, p = primary key constraint, u = unique constraint */
|
104
|
+
when contype = 'c' then 'check_constraint'
|
105
|
+
when contype = 'f' then 'foreign key constraint'
|
106
|
+
when contype = 'p' then 'primary key constraint'
|
107
|
+
when contype = 'u' then 'unique constraint'
|
108
|
+
else 'unknown constraint'
|
109
|
+
end as contype
|
110
|
+
,foreign_class.relname as foreign_table_name
|
111
|
+
,conkey as attribute_column_numbers
|
112
|
+
,confkey as foreign_attribute_column_numbers
|
113
|
+
,consrc as condition_decleration_src
|
114
|
+
,conkey[0] = 0 as problem
|
115
|
+
|
116
|
+
|
117
|
+
from pg_constraint
|
118
|
+
|
119
|
+
left join pg_class as foreign_class on foreign_class.oid = confrelid
|
120
|
+
|
121
|
+
|
122
|
+
where
|
123
|
+
|
124
|
+
conrelid = #{table_oid}
|
125
|
+
|
126
|
+
union
|
127
|
+
|
128
|
+
SELECT
|
129
|
+
|
130
|
+
i.relname AS conname
|
131
|
+
,'unique constraint' as contype
|
132
|
+
,'' as foreign_table_name
|
133
|
+
,indkey as attribute_column_numbers
|
134
|
+
,null as foreign_attribute_column_numbers
|
135
|
+
,pg_get_indexdef(i.oid) as condition_decleration_src
|
136
|
+
,indkey[0] = 0 as problem
|
137
|
+
|
138
|
+
FROM pg_index x
|
139
|
+
JOIN pg_class c ON c.oid = x.indrelid
|
140
|
+
JOIN pg_class i ON i.oid = x.indexrelid
|
141
|
+
LEFT JOIN pg_tablespace t ON t.oid = i.reltablespace
|
142
|
+
WHERE i.relkind = 'i' and c.oid = #{table_oid} and x.indisunique = true and x.indisprimary = false
|
143
|
+
|
144
|
+
) as bla
|
145
|
+
|
146
|
+
order by conname,contype,foreign_table_name,attribute_column_numbers,condition_decleration_src,problem").each do |row|
|
147
|
+
unless row['foreign_table_name'].nil? || row['foreign_table_name'].empty? || the_tables.include?(row['foreign_table_name'])
|
148
|
+
deps[row['foreign_table_name']] = row['foreign_table_name']
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
deps_tables = deps.keys
|
154
|
+
|
155
|
+
if deps_tables.length > 0 && regex
|
156
|
+
puts "\n\tWill also create models for these tables as other tables depend on them\n\n"
|
157
|
+
deps_tables.each do |table_name|
|
158
|
+
puts "\t\t#{table_name}"
|
159
|
+
end
|
160
|
+
puts "\n"
|
161
|
+
end
|
162
|
+
|
163
|
+
result.each do |row|
|
164
|
+
|
165
|
+
if @regex.match(row['relname']) || deps_tables.include?(row['relname'])
|
166
|
+
allModelsContent[row['relname']] = {:constraint => "",:set => "", :binding => "", :columns => ""}
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
result.each do |row|
|
172
|
+
|
173
|
+
if @regex.match(row['relname']) || deps_tables.include?(row['relname'])
|
174
|
+
get_columns( row['relname'],row['oid'] ,con,allModelsContent)
|
175
|
+
get_constraints( row['relname'],row['oid'] ,con,allModelsContent)
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
allModelsContent.each do |key,value|
|
181
|
+
|
182
|
+
class_name = table2class(check_class_name(key.capitalize));
|
183
|
+
|
184
|
+
model_file_name = "./app/models/"+camle_case_2_file_name(@model_name ? @model_name + "/" : '')+"#{camle_case_2_file_name(class_name)}.rb".downcase
|
185
|
+
|
186
|
+
FileUtils.makedirs(File.dirname(model_file_name))
|
187
|
+
|
188
|
+
value[:set] = "\tset_table_name \"#{key}\"\n" + value[:set].to_s
|
189
|
+
|
190
|
+
first_part = "# encoding: utf-8\n\n"
|
191
|
+
first_part += "class #{get_class_name_with_namespace(class_name)} < ActiveRecord::Base \n"
|
192
|
+
first_part += "\n"
|
193
|
+
first_part += "# --- auto_gen_start ---\n"
|
194
|
+
|
195
|
+
last_part = "# --- auto_gen_end ---\n"
|
196
|
+
last_part += "\n"
|
197
|
+
last_part += "end \n"
|
198
|
+
last_part += "\n"
|
199
|
+
|
200
|
+
|
201
|
+
if File.exist?(model_file_name)
|
202
|
+
|
203
|
+
file_data = File.read(model_file_name)
|
204
|
+
|
205
|
+
if file_data.split(/\t#--- auto_gen_start ---/m).length == 2
|
206
|
+
|
207
|
+
first_part = file_data.split(/\t#--- auto_gen_start ---/m)[0]
|
208
|
+
first_part += "\t#--- auto_gen_start ---\n"
|
209
|
+
|
210
|
+
last_part = "\t#--- auto_gen_end ---"
|
211
|
+
last_part += file_data.split(/\t#--- auto_gen_end ---/m)[1].to_s
|
212
|
+
|
213
|
+
# Support for the old layout
|
214
|
+
elsif file_data.split(/# --- auto_gen_start ---/m).length == 2
|
215
|
+
|
216
|
+
first_part = file_data.split(/# --- auto_gen_start ---/m)[0]
|
217
|
+
first_part += "\t#--- auto_gen_start ---\n"
|
218
|
+
|
219
|
+
last_part = "\t#--- auto_gen_end ---"
|
220
|
+
last_part += file_data.split(/# --- auto_gen_end ---/m)[1].to_s
|
221
|
+
|
222
|
+
else
|
223
|
+
|
224
|
+
first_part = file_data.split(/end[\s]*\z/m)[0]
|
225
|
+
first_part += "\n\n\n"
|
226
|
+
first_part += "\t#--- auto_gen_start ---\n"
|
227
|
+
|
228
|
+
last_part = "\t#--- auto_gen_end ---\n"
|
229
|
+
last_part += "end\n"
|
230
|
+
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
|
235
|
+
first_part += "\t#\n"
|
236
|
+
first_part += "\t# This is generate using gen_pg_models, dont make changes\n"
|
237
|
+
first_part += "\t# within auto_gen_XXXXX as it will be overwriten next time\n"
|
238
|
+
first_part += "\t# gen_pg_models is run.\n"
|
239
|
+
first_part += "\t#\n"
|
240
|
+
|
241
|
+
puts "\tWriting to #{model_file_name}"
|
242
|
+
File.open(model_file_name,'w') do |io|
|
243
|
+
|
244
|
+
io.write(first_part)
|
245
|
+
io.write("\n\t# Columns\n")
|
246
|
+
io.write("\n\t"+value[:columns].to_s.strip+"\n")
|
247
|
+
io.write("\n\t# Table config \n")
|
248
|
+
io.write("\n\t"+value[:set].to_s.strip+"\n")
|
249
|
+
io.write("\n\t# Constraints \n")
|
250
|
+
io.write("\n\t"+(value[:constraint].to_s.strip)+"\n")
|
251
|
+
io.write("\n\t# Foreign keys \n")
|
252
|
+
io.write("\n\t"+value[:binding].to_s.strip+"\n\n")
|
253
|
+
io.write(last_part)
|
254
|
+
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
puts "\n"
|
259
|
+
end
|
260
|
+
|
261
|
+
|
262
|
+
def get_class_name_with_namespace class_name
|
263
|
+
|
264
|
+
unless @model_name.nil? || @model_name.empty?
|
265
|
+
@model_name + "::" + class_name
|
266
|
+
else
|
267
|
+
class_name
|
268
|
+
end
|
269
|
+
|
270
|
+
end
|
271
|
+
|
272
|
+
|
273
|
+
def check_class_name class_name
|
274
|
+
|
275
|
+
unusable_class_names = ['Thread','thread']
|
276
|
+
|
277
|
+
if unusable_class_names.include? class_name
|
278
|
+
class_name+"oooo"
|
279
|
+
else
|
280
|
+
class_name
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
|
285
|
+
def is_there_an_index_on table_name,column_name,connection
|
286
|
+
|
287
|
+
result = connection.query "
|
288
|
+
|
289
|
+
select
|
290
|
+
|
291
|
+
pg_class.relname as table_name
|
292
|
+
,pg_attribute.attname as column_name
|
293
|
+
,case when pg_index.indkey is not null then 't' else 'f' end as has_index
|
294
|
+
|
295
|
+
from pg_attribute
|
296
|
+
|
297
|
+
join pg_class on pg_class.oid = pg_attribute.attrelid
|
298
|
+
left join pg_index on pg_index.indrelid = pg_class.oid and array_to_string(pg_index.indkey,',') ilike '%' || pg_attribute.attnum || '%'
|
299
|
+
|
300
|
+
where
|
301
|
+
|
302
|
+
pg_class.relname = '#{table_name.downcase}' and pg_attribute.attname = '#{column_name.downcase}' and pg_index.indkey is not null"
|
303
|
+
|
304
|
+
result.ntuples > 0
|
305
|
+
end
|
306
|
+
|
307
|
+
|
308
|
+
def table2class string
|
309
|
+
string.camelize.singularize
|
310
|
+
end
|
311
|
+
|
312
|
+
def pad_string string,min_width
|
313
|
+
while string.length < min_width
|
314
|
+
string = string + " "
|
315
|
+
end
|
316
|
+
|
317
|
+
string
|
318
|
+
end
|
319
|
+
|
320
|
+
def camle_case_2_file_name string
|
321
|
+
string.underscore
|
322
|
+
end
|
323
|
+
|
324
|
+
def get_attribute_name attribute_number,table_oid,connection
|
325
|
+
|
326
|
+
result = connection.query "
|
327
|
+
select
|
328
|
+
|
329
|
+
pg_attribute.attnum as attribute_number
|
330
|
+
,pg_type.typname as type_name
|
331
|
+
,pg_attribute.attname as attribute_name
|
332
|
+
,pg_type.typlen as max_length
|
333
|
+
,pg_attribute.atttypmod as max_length_minus_4_for_varlen
|
334
|
+
,pg_attribute.attnotnull as not_null_constraint
|
335
|
+
,pg_attribute.attisdropped as attisdropped /* This column has been dropped and is no longer valid. A dropped column is still physically present in the table, but is ignored by the parser and so cannot be accessed via SQL. */
|
336
|
+
,pg_attribute.atthasdef as has_default_value
|
337
|
+
,pg_attrdef.adsrc as default_value_src
|
338
|
+
|
339
|
+
from pg_attribute
|
340
|
+
|
341
|
+
join pg_type on pg_type.oid = pg_attribute.atttypid
|
342
|
+
left join pg_attrdef on pg_attrdef.adrelid = pg_attribute.attrelid and pg_attrdef.adnum = pg_attribute.attnum
|
343
|
+
|
344
|
+
where
|
345
|
+
pg_attribute.attrelid = #{table_oid}
|
346
|
+
and pg_attribute.attnum = #{attribute_number}"
|
347
|
+
|
348
|
+
result.first['attribute_name']
|
349
|
+
end
|
350
|
+
|
351
|
+
def get_attribute_row attribute_number,table_oid,connection
|
352
|
+
|
353
|
+
result = connection.query "
|
354
|
+
select
|
355
|
+
|
356
|
+
pg_attribute.attnum as attribute_number
|
357
|
+
,pg_type.typname as type_name
|
358
|
+
,pg_attribute.attname as attribute_name
|
359
|
+
,pg_type.typlen as max_length
|
360
|
+
,pg_attribute.atttypmod as max_length_minus_4_for_varlen
|
361
|
+
,pg_attribute.attnotnull as not_null_constraint
|
362
|
+
,pg_attribute.attisdropped as attisdropped /* This column has been dropped and is no longer valid. A dropped column is still physically present in the table, but is ignored by the parser and so cannot be accessed via SQL. */
|
363
|
+
,pg_attribute.atthasdef as has_default_value
|
364
|
+
,pg_attrdef.adsrc as default_value_src
|
365
|
+
|
366
|
+
from pg_attribute
|
367
|
+
|
368
|
+
join pg_type on pg_type.oid = pg_attribute.atttypid
|
369
|
+
left join pg_attrdef on pg_attrdef.adrelid = pg_attribute.attrelid and pg_attrdef.adnum = pg_attribute.attnum
|
370
|
+
|
371
|
+
where
|
372
|
+
pg_attribute.attrelid = #{table_oid}
|
373
|
+
and pg_attribute.attnum = #{attribute_number}"
|
374
|
+
|
375
|
+
result.first
|
376
|
+
end
|
377
|
+
|
378
|
+
def get_table_oid table_name,connection
|
379
|
+
|
380
|
+
result = connection.query "
|
381
|
+
select
|
382
|
+
|
383
|
+
pg_class.oid
|
384
|
+
|
385
|
+
from pg_class
|
386
|
+
|
387
|
+
join pg_namespace on pg_namespace.oid = pg_class.relnamespace
|
388
|
+
|
389
|
+
where
|
390
|
+
|
391
|
+
relkind = 'r' /* r = ordinary table, i = index, S = sequence, v = view, c = composite type, s = special, t = TOAST table */
|
392
|
+
and pg_namespace.nspname = '#{@schema_name}'
|
393
|
+
and pg_class.relname = '#{table_name}'
|
394
|
+
"
|
395
|
+
|
396
|
+
result.first['oid']
|
397
|
+
end
|
398
|
+
|
399
|
+
|
400
|
+
def get_table_name table_oid,connection
|
401
|
+
|
402
|
+
result = connection.query "
|
403
|
+
select
|
404
|
+
|
405
|
+
pg_class.relname
|
406
|
+
|
407
|
+
from pg_class
|
408
|
+
|
409
|
+
join pg_namespace on pg_namespace.oid = pg_class.relnamespace
|
410
|
+
|
411
|
+
where
|
412
|
+
|
413
|
+
relkind = 'r' /* r = ordinary table, i = index, S = sequence, v = view, c = composite type, s = special, t = TOAST table */
|
414
|
+
and pg_namespace.nspname = '#{@schema_name}'
|
415
|
+
and pg_class.oid = #{table_oid}
|
416
|
+
"
|
417
|
+
|
418
|
+
result.first['relname']
|
419
|
+
end
|
420
|
+
|
421
|
+
def get_default_value_src attribute_number,table_oid,connection
|
422
|
+
|
423
|
+
result = connection.query "
|
424
|
+
select
|
425
|
+
|
426
|
+
pg_attribute.attnum as attribute_number
|
427
|
+
,pg_type.typname as type_name
|
428
|
+
,pg_attribute.attname as attribute_name
|
429
|
+
,pg_type.typlen as max_length
|
430
|
+
,pg_attribute.atttypmod as max_length_minus_4_for_varlen
|
431
|
+
,pg_attribute.attnotnull as not_null_constraint
|
432
|
+
,pg_attribute.attisdropped as attisdropped /* This column has been dropped and is no longer valid. A dropped column is still physically present in the table, but is ignored by the parser and so cannot be accessed via SQL. */
|
433
|
+
,pg_attribute.atthasdef as has_default_value
|
434
|
+
,pg_attrdef.adsrc as default_value_src
|
435
|
+
|
436
|
+
from pg_attribute
|
437
|
+
|
438
|
+
join pg_type on pg_type.oid = pg_attribute.atttypid
|
439
|
+
left join pg_attrdef on pg_attrdef.adrelid = pg_attribute.attrelid and pg_attrdef.adnum = pg_attribute.attnum
|
440
|
+
|
441
|
+
where
|
442
|
+
pg_attribute.attrelid = #{table_oid}
|
443
|
+
and pg_attribute.attnum = #{attribute_number}"
|
444
|
+
|
445
|
+
result.first['default_value_src']
|
446
|
+
end
|
447
|
+
|
448
|
+
def get_constraints table_name,table_oid,connection,models
|
449
|
+
|
450
|
+
result = connection.query "
|
451
|
+
|
452
|
+
|
453
|
+
|
454
|
+
|
455
|
+
select * from
|
456
|
+
|
457
|
+
(
|
458
|
+
|
459
|
+
select
|
460
|
+
|
461
|
+
conname
|
462
|
+
,case /* c = check constraint, f = foreign key constraint, p = primary key constraint, u = unique constraint */
|
463
|
+
when contype = 'c' then 'check_constraint'
|
464
|
+
when contype = 'f' then 'foreign key constraint'
|
465
|
+
when contype = 'p' then 'primary key constraint'
|
466
|
+
when contype = 'u' then 'unique constraint'
|
467
|
+
else 'unknown constraint'
|
468
|
+
end as contype
|
469
|
+
,foreign_class.relname as foreign_table_name
|
470
|
+
,conkey as attribute_column_numbers
|
471
|
+
,confkey as foreign_attribute_column_numbers
|
472
|
+
,consrc as condition_decleration_src
|
473
|
+
,conkey[0] = 0 as problem
|
474
|
+
|
475
|
+
|
476
|
+
from pg_constraint
|
477
|
+
|
478
|
+
left join pg_class as foreign_class on foreign_class.oid = confrelid
|
479
|
+
|
480
|
+
|
481
|
+
where
|
482
|
+
|
483
|
+
conrelid = #{table_oid}
|
484
|
+
|
485
|
+
union
|
486
|
+
|
487
|
+
SELECT
|
488
|
+
|
489
|
+
i.relname AS conname
|
490
|
+
,'unique constraint' as contype
|
491
|
+
,'' as foreign_table_name
|
492
|
+
,indkey as attribute_column_numbers
|
493
|
+
,null as foreign_attribute_column_numbers
|
494
|
+
,pg_get_indexdef(i.oid) as condition_decleration_src
|
495
|
+
,indkey[0] = 0 as problem
|
496
|
+
|
497
|
+
FROM pg_index x
|
498
|
+
JOIN pg_class c ON c.oid = x.indrelid
|
499
|
+
JOIN pg_class i ON i.oid = x.indexrelid
|
500
|
+
LEFT JOIN pg_tablespace t ON t.oid = i.reltablespace
|
501
|
+
WHERE i.relkind = 'i' and c.oid = #{table_oid} and x.indisunique = true and x.indisprimary = false
|
502
|
+
|
503
|
+
) as bla
|
504
|
+
|
505
|
+
order by conname,attribute_column_numbers,foreign_table_name"
|
506
|
+
|
507
|
+
#
|
508
|
+
# Set serial for primary key
|
509
|
+
#
|
510
|
+
result.each do |row|
|
511
|
+
if row['contype'] == 'primary key constraint' && row['problem'] != 't'
|
512
|
+
|
513
|
+
columns = row['attribute_column_numbers'].gsub("{","").gsub("}","").split(',');
|
514
|
+
match = get_default_value_src(columns.first,table_oid,connection).to_s.match("\'([a-zA-Z_]*)\'");
|
515
|
+
|
516
|
+
models[table_name][:set] += "\tset_primary_key \"#{get_attribute_name(columns.first,table_oid,connection)}\"\n"
|
517
|
+
models[table_name][:set] += "\tset_sequence_name \"#{match[1]}\"\n" if match
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
#
|
522
|
+
# set unique canstraints
|
523
|
+
#
|
524
|
+
temp_constraints = {}
|
525
|
+
models[table_name][:constraint] += "\n"
|
526
|
+
result.each do |row|
|
527
|
+
if row['contype'] == 'unique constraint' && row['problem'] != 't'
|
528
|
+
|
529
|
+
if row['attribute_column_numbers'].include? "="
|
530
|
+
row['attribute_column_numbers'] = row['attribute_column_numbers'].split('=')[1]
|
531
|
+
end
|
532
|
+
|
533
|
+
columns = row['attribute_column_numbers'].gsub("{","").gsub("}","").split(',');
|
534
|
+
column_rows = columns.map do |value|
|
535
|
+
get_attribute_row(value,table_oid,connection)
|
536
|
+
end
|
537
|
+
|
538
|
+
allow_nil = ((column_rows.first['not_null_constraint'] != 't' or column_rows.first['has_default_value'] == 't') ? ", :allow_nil => true" : "")
|
539
|
+
|
540
|
+
|
541
|
+
columns.map! do |value|
|
542
|
+
":"+get_attribute_name(value,table_oid,connection)
|
543
|
+
end
|
544
|
+
|
545
|
+
string = ""
|
546
|
+
if columns.length > 1
|
547
|
+
#
|
548
|
+
# validates_uniqueness_of with scope dosent want to work at the moment so we add it be commented out.
|
549
|
+
# It will sit there as a reminder that it could work if only ActiveRecord would work a bit more like i expect it to
|
550
|
+
#
|
551
|
+
string = "\t#validates_uniqueness_of #{columns.shift}, :scope => [#{columns.join(',')}]#{allow_nil}\n"
|
552
|
+
else
|
553
|
+
string = "\tvalidates_uniqueness_of #{columns.first}#{allow_nil}\n"
|
554
|
+
end
|
555
|
+
|
556
|
+
temp_constraints[string] = string
|
557
|
+
end
|
558
|
+
end
|
559
|
+
models[table_name][:constraint] += temp_constraints.keys.join("")
|
560
|
+
|
561
|
+
|
562
|
+
|
563
|
+
#
|
564
|
+
# Warn about prolems
|
565
|
+
#
|
566
|
+
models[table_name][:constraint] += "\n"
|
567
|
+
result.each do |row|
|
568
|
+
if row['problem'] == 't'
|
569
|
+
|
570
|
+
models[table_name][:constraint] += "# cant do anything with this\n"
|
571
|
+
models[table_name][:constraint] += "# #{row['condition_decleration_src']}\n"
|
572
|
+
models[table_name][:constraint] += "# validates_uniqueness_of ???\n"
|
573
|
+
|
574
|
+
puts "\n WARNING! cant do anything with this \n"
|
575
|
+
puts "#{row['condition_decleration_src']}\n\n"
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
|
580
|
+
|
581
|
+
#
|
582
|
+
# set check constraints
|
583
|
+
#
|
584
|
+
models[table_name][:constraint] += "\n"
|
585
|
+
result.each do |row|
|
586
|
+
if row['contype'] == 'check_constraint' && row['problem'] != 't'
|
587
|
+
|
588
|
+
columns = row['attribute_column_numbers'].gsub("{","").gsub("}","").split(',');
|
589
|
+
column_rows = columns.map do |value|
|
590
|
+
get_attribute_row(value,table_oid,connection)
|
591
|
+
end
|
592
|
+
|
593
|
+
allow_nil = ((column_rows.first['not_null_constraint'] != 't' or column_rows.first['has_default_value'] == 't') ? ", :allow_nil => true" : "")
|
594
|
+
|
595
|
+
if row['condition_decleration_src'].match('\)::text ~\* \'(.*)\'::text\)$')
|
596
|
+
|
597
|
+
# Regex check constraint
|
598
|
+
match = row['condition_decleration_src'].match('\)::text ~\* \'(.*)\'::text\)$')
|
599
|
+
models[table_name][:constraint] += "\tvalidates_format_of :#{column_rows.first["attribute_name"]}, :with => /#{match[1].gsub('\\\\.','').gsub('/','\/')}/i#{allow_nil}\n"
|
600
|
+
|
601
|
+
elsif row['condition_decleration_src'].match(/\(char_length\((.*)::text\) >= ([0-9]*)\)/)
|
602
|
+
|
603
|
+
# Length constraint
|
604
|
+
match = row['condition_decleration_src'].match(/^\(char_length\(\((.*)\)::text\) >= ([0-9]*)\)$/)
|
605
|
+
models[table_name][:constraint] += "\tvalidates_length_of :#{match[1]}, :minimum => #{match[2]}\n"
|
606
|
+
|
607
|
+
else
|
608
|
+
|
609
|
+
models[table_name][:constraint] += "\t# unknown check constraint\n"
|
610
|
+
models[table_name][:constraint] += "\t# #{row['condition_decleration_src']}\n"
|
611
|
+
end
|
612
|
+
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
|
617
|
+
|
618
|
+
|
619
|
+
#
|
620
|
+
# Add foreign keys
|
621
|
+
#
|
622
|
+
models[table_name][:binding] += "\n"
|
623
|
+
result.each do |row|
|
624
|
+
if row['contype'] == 'foreign key constraint' && row['problem'] != 't'
|
625
|
+
|
626
|
+
local_columns = row['attribute_column_numbers'].gsub("{","").gsub("}","").split(',');
|
627
|
+
local_columns.map! do |value|
|
628
|
+
get_attribute_name(value,table_oid,connection)
|
629
|
+
end
|
630
|
+
|
631
|
+
foreign_oid = get_table_oid(row['foreign_table_name'],connection)
|
632
|
+
foreign_columns = row['foreign_attribute_column_numbers'].gsub("{","").gsub("}","").split(',');
|
633
|
+
foreign_columns.map! do |value|
|
634
|
+
get_attribute_name(value,foreign_oid,connection)
|
635
|
+
end
|
636
|
+
|
637
|
+
association_name = "fkey____#{table_name.upcase}_#{local_columns.first}____#{row['foreign_table_name'].upcase}_#{foreign_columns.first}____"
|
638
|
+
|
639
|
+
unless is_there_an_index_on(table_name,local_columns.first,connection)
|
640
|
+
models[table_name][:binding] += "\t# WARNING! might result in a slow query. #{association_name} is missing a index on the foreign_key\n"
|
641
|
+
models[row['foreign_table_name']][:binding] += "\t# WARNING! might result in a slow query. #{association_name} is missing a index on the foreign_key\n"
|
642
|
+
end
|
643
|
+
|
644
|
+
models[table_name][:binding] += "\tbelongs_to :#{association_name}, :foreign_key => :#{local_columns.first}, :primary_key => :#{foreign_columns.first}, :class_name => \"#{get_class_name_with_namespace(table2class(check_class_name(row['foreign_table_name'])))}\"\n"
|
645
|
+
models[row['foreign_table_name']][:binding] += "\thas_many :#{association_name}, :foreign_key => :#{local_columns.first}, :primary_key => :#{foreign_columns.first}, :class_name => \"#{get_class_name_with_namespace(table2class(check_class_name(table_name)))}\"\n"
|
646
|
+
|
647
|
+
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
end
|
652
|
+
|
653
|
+
def get_columns table_name,oid,connection,models
|
654
|
+
|
655
|
+
result = connection.query "
|
656
|
+
select
|
657
|
+
|
658
|
+
pg_attribute.attnum as attribute_number
|
659
|
+
,pg_type.typname as type_name
|
660
|
+
,pg_attribute.attname as attribute_name
|
661
|
+
,pg_type.typlen as max_length
|
662
|
+
,pg_attribute.atttypmod as max_length_minus_4_for_varlen
|
663
|
+
,pg_attribute.attnotnull as not_null_constraint
|
664
|
+
,pg_attribute.attisdropped as attisdropped /* This column has been dropped and is no longer valid. A dropped column is still physically present in the table, but is ignored by the parser and so cannot be accessed via SQL. */
|
665
|
+
,pg_attribute.atthasdef as has_default_value
|
666
|
+
,pg_attrdef.adsrc as default_value_src
|
667
|
+
|
668
|
+
from pg_attribute
|
669
|
+
|
670
|
+
join pg_type on pg_type.oid = pg_attribute.atttypid
|
671
|
+
left join pg_attrdef on pg_attrdef.adrelid = pg_attribute.attrelid and pg_attrdef.adnum = pg_attribute.attnum
|
672
|
+
|
673
|
+
where
|
674
|
+
pg_attribute.attrelid = #{oid}
|
675
|
+
and pg_attribute.attnum > 0
|
676
|
+
|
677
|
+
order by attribute_name;
|
678
|
+
"
|
679
|
+
|
680
|
+
columns_buffer = ""
|
681
|
+
|
682
|
+
#
|
683
|
+
# Add comment with the column names
|
684
|
+
#
|
685
|
+
result.each do |row|
|
686
|
+
if row['has_default_value'] == 't'
|
687
|
+
models[table_name][:columns] += "\t#\t\t#{pad_string(row['attribute_name'],20) }\t#{pad_string(row['type_name'],10)}\t #{row['default_value_src']} \n"
|
688
|
+
else
|
689
|
+
models[table_name][:columns] += "\t#\t\t#{pad_string(row['attribute_name'],20) }\t#{pad_string(row['type_name'],10)} \n"
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
#
|
694
|
+
# Add not null constraints
|
695
|
+
#
|
696
|
+
models[table_name][:constraint] += "\n"
|
697
|
+
result.each do |row|
|
698
|
+
if row['not_null_constraint'] == 't' and row['has_default_value'] == 'f'
|
699
|
+
if row['type_name'] == 'bool'
|
700
|
+
else
|
701
|
+
models[table_name][:constraint] += "\tvalidates_presence_of :#{row['attribute_name']} \n"
|
702
|
+
end
|
703
|
+
end
|
704
|
+
end
|
705
|
+
|
706
|
+
models[table_name][:constraint] += "\n" unless models[table_name][:constraint][-1,1] == "\n"
|
707
|
+
result.each do |row|
|
708
|
+
if row['not_null_constraint'] == 't' and row['has_default_value'] == 'f'
|
709
|
+
if row['type_name'] == 'bool'
|
710
|
+
models[table_name][:constraint] += "\tvalidates_inclusion_of :#{row['attribute_name']}, :in => [true, false] \n"
|
711
|
+
else
|
712
|
+
end
|
713
|
+
end
|
714
|
+
end
|
715
|
+
|
716
|
+
#
|
717
|
+
# Add varchar length constraints
|
718
|
+
#
|
719
|
+
models[table_name][:constraint] += "\n"
|
720
|
+
result.each do |row|
|
721
|
+
if row['max_length_minus_4_for_varlen'].to_i > 0
|
722
|
+
allow_nil = ((row['not_null_constraint'] != 't' or row['has_default_value'] == 't') ? ",:allow_nil => true" : "")
|
723
|
+
models[table_name][:constraint] += "\tvalidates_length_of :#{row['attribute_name']}, :maximum => #{row['max_length_minus_4_for_varlen'].to_i - 4} #{allow_nil} \n"
|
724
|
+
end
|
725
|
+
end
|
726
|
+
|
727
|
+
#
|
728
|
+
# Add integer constraints
|
729
|
+
#
|
730
|
+
models[table_name][:constraint] += "\n"
|
731
|
+
result.each do |row|
|
732
|
+
if row['type_name'].include? "int"
|
733
|
+
allow_nil = ((row['not_null_constraint'] != 't' or row['has_default_value'] == 't') ? ",:allow_nil => true" : "")
|
734
|
+
models[table_name][:constraint] += "\tvalidates_numericality_of :#{row['attribute_name']}, :only_integer => true #{allow_nil} \n"
|
735
|
+
end
|
736
|
+
end
|
737
|
+
|
738
|
+
end
|
739
|
+
|
740
|
+
end
|
741
|
+
|
742
|
+
|
743
|
+
namespace :db do
|
744
|
+
|
745
|
+
desc 'Generates/updates activerecord models based on current schema in the postgresql db'
|
746
|
+
task :gen_models => :environment do |t,args|
|
747
|
+
|
748
|
+
args = args.to_hash
|
749
|
+
|
750
|
+
if args[:dbname].blank?
|
751
|
+
args[:dbname] = ActiveRecord::Base.connection.current_database
|
752
|
+
end
|
753
|
+
|
754
|
+
args[:schema_name] = 'public' if args[:schema_name].nil? || args[:schema_name].empty?
|
755
|
+
|
756
|
+
schema_name = args[:schema_name]
|
757
|
+
dbname = args[:dbname]
|
758
|
+
prefix = (args[:prefix]||'').capitalize
|
759
|
+
|
760
|
+
PGGen.new(prefix,schema_name,dbname)
|
761
|
+
end
|
762
|
+
|
763
|
+
desc 'Generates/updates activerecord models based on current schema in the postgresql db for tables matching the supplied regex'
|
764
|
+
task :gen_matching_models,[:regex] => [:environment] do |t,args|
|
765
|
+
args = args.to_hash
|
766
|
+
|
767
|
+
if args[:dbname].blank?
|
768
|
+
args[:dbname] = ActiveRecord::Base.connection.current_database
|
769
|
+
end
|
770
|
+
|
771
|
+
args[:schema_name] = 'public' if args[:schema_name].nil? || args[:schema_name].empty?
|
772
|
+
|
773
|
+
schema_name = args[:schema_name]
|
774
|
+
dbname = args[:dbname]
|
775
|
+
prefix = (args[:prefix]||'').capitalize
|
776
|
+
|
777
|
+
PGGen.new(prefix,schema_name,dbname,args[:regex])
|
778
|
+
end
|
779
|
+
|
780
|
+
|
781
|
+
end
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pgmodelgen
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.5
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Bjorn Blomqvist
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: jeweler
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: Rake task that generates/updates activerecord models based on current
|
47
|
+
schema in the postgresql DB
|
48
|
+
email: darwin@bits2life.com
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files:
|
52
|
+
- LICENSE.txt
|
53
|
+
- README.rdoc
|
54
|
+
files:
|
55
|
+
- lib/pgmodelgen.rb
|
56
|
+
- lib/pgmodelgen/railtie.rb
|
57
|
+
- lib/tasks/pgmodelgen.rake
|
58
|
+
- LICENSE.txt
|
59
|
+
- README.rdoc
|
60
|
+
homepage: http://github.com/bjornblomqvist/pgmodelgen
|
61
|
+
licenses:
|
62
|
+
- MIT
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ! '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
hash: -2115245809043789605
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 1.8.24
|
85
|
+
signing_key:
|
86
|
+
specification_version: 3
|
87
|
+
summary: Rake task that generates/updates activerecord models based on current schema
|
88
|
+
in the postgresql DB
|
89
|
+
test_files: []
|