pgmodelgen 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
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
+
@@ -0,0 +1,11 @@
1
+ require 'pgmodelgen'
2
+ require 'rails'
3
+ module Pgmodelgen
4
+ class Railtie < Rails::Railtie
5
+ railtie_name :pgmodelgen
6
+
7
+ rake_tasks do
8
+ load "tasks/pgmodelgen.rake"
9
+ end
10
+ end
11
+ end
data/lib/pgmodelgen.rb ADDED
@@ -0,0 +1,4 @@
1
+
2
+ module Pgmodelgen
3
+ require 'pgmodelgen/railtie' if defined?(Rails)
4
+ end
@@ -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: []