Class: ActiveFacts::Generators::Rails::SchemaRb

Inherits:
Object
  • Object
show all
Includes:
RMap
Defined in:
lib/activefacts/generators/rails/schema.rb

Overview

Generate a Rails-friendly schema for the vocabulary Invoke as

afgen --rails/schema[=options] <file>.cql

Instance Method Summary collapse

Methods included from RMap

rails_name_trunc, rails_plural_name, rails_singular_name

Instance Method Details

#generate(out = $>) ⇒ Object

:nodoc:



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/activefacts/generators/rails/schema.rb', line 186

def generate(out = $>)      #:nodoc:
  return if @helping
  @out = out

  foreign_keys = []

  # If we get index names that need to be truncated, add a counter to ensure uniqueness
  dup_id = 0

  puts "#\n# schema.rb auto-generated using ActiveFacts for #{@vocabulary.name} on #{Date.today}\n#\n\n"
  puts "ActiveRecord::Base.logger = Logger.new(STDOUT)\n"
  puts "ActiveRecord::Schema.define(version: #{Time.now.strftime('%Y%m%d%H%M%S')}) do"
  puts "  enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')\n"

  @vocabulary.tables.each do |table|
    generate_table table, foreign_keys
  end

  unless @exclude_fks
    puts '  unless ENV["EXCLUDE_FKS"]'
    puts foreign_keys.join("\n")
    puts '  end'
  end
  puts "end"
end

#generate_column(table, pk, column) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/activefacts/generators/rails/schema.rb', line 68

def generate_column table, pk, column
  name = column.rails_name
  type, params, constraints = *column.type
  length = params[:length]
  length &&= length.to_i
  scale = params[:scale]
  scale &&= scale.to_i
  rails_type, length = *column.rails_type
 
  length_name = rails_type == 'decimal' ? 'precision' : 'limit'
  length_option = length ? ", #{length_name}: #{length}" : ''
  scale_option = scale ? ", scale: #{scale}" : ''

  comment = column.comment
  null_option = ", null: #{!column.is_mandatory}"
  if pk.size == 1 && pk[0] == column
    case rails_type
    when 'serial'
      rails_type = "primary_key"
    when 'uuid'
      rails_type = "uuid, default: 'gen_random_uuid()', primary_key: true"
    end
  else
    case rails_type
    when 'serial'
      rails_type = 'integer'        # An integer foreign key
    end
  end

  (@include_comments ? ["    \# #{comment}"] : []) +
  [
    %Q{    t.column "#{name}", :#{rails_type}#{length_option}#{scale_option}#{null_option}}
  ]
end

#generate_columns(table, pk, fk_columns) ⇒ Object



103
104
105
106
107
108
109
# File 'lib/activefacts/generators/rails/schema.rb', line 103

def generate_columns table, pk, fk_columns
  sc = sorted_columns(table, pk, fk_columns)
  lines = sc.map do |column|
    generate_column table, pk, column
  end
  lines.flatten
end

#generate_table(table, foreign_keys) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/activefacts/generators/rails/schema.rb', line 111

def generate_table table, foreign_keys
  ar_table_name = table.rails_name

  pk = table.identifier_columns
  if pk[0].is_auto_assigned
    identity_column = pk[0]
    warn "Warning: redundant column(s) after #{identity_column.name} in primary key of #{ar_table_name}" if pk.size > 1
  end

  # Get the list of references that give rise to foreign keys:
  fk_refs = table.references_from.select{|ref| ref.is_simple_reference }

  # Get the list of columns that embody the foreign keys:
  fk_columns = table.columns.select do |column|
    column.references[0].is_simple_reference
  end

  # Detect if this table is a join table.
  # Join tables have multi-part primary keys that are made up only of foreign keys
  is_join_table = pk.length > 1 and
    !pk.detect do |pk_column|
      !fk_columns.include?(pk_column)
    end
  warn "Warning: #{table.name} has a multi-part primary key" if pk.length > 1 and !is_join_table

  puts %Q{  create_table "#{ar_table_name}", id: false, force: true do |t|}

  columns = generate_columns table, pk, fk_columns

  unless @exclude_fks
    table.foreign_keys.each do |fk|
      from_columns = fk.from_columns.map{|column| column.rails_name}
      to_columns = fk.to_columns.map{|column| column.rails_name}

      foreign_keys.concat(
        if (from_columns.length == 1)
          index_name = RMap.rails_name_trunc('index_'+fk.from.rails_name+'_on_'+from_columns[0])
          [
            "    add_foreign_key :#{fk.from.rails_name}, :#{fk.to.rails_name}, column: :#{from_columns[0]}, primary_key: :#{to_columns[0]}, on_delete: :cascade"
          ]+
          Array(
            # Index it non-uniquely only if it's not unique already:
            fk.jump_reference.to_role.is_unique ? nil :
              "    add_index :#{fk.from.rails_name}, [:#{from_columns[0]}], unique: false, name: :#{index_name}"
          )
        else
          # This probably isn't going to work without Dr Nic's CPK gem:
          [
            "    add_foreign_key :#{fk.to.rails_name}, :#{fk.from.rails_name}, column: [:#{from_columns.join(':, ')}], primary_key: [:#{to_columns.join(':, ')}], on_delete: :cascade"
          ]
        end
      )
    end
  end

  indices = table.indices
  index_text = []
  indices.each do |index|
    next if index.is_primary && index.columns.size == 1   # We've handled this already

    index_name = index.rails_name

    unique = !index.columns.detect{|column| !column.is_mandatory} and !@closed_world
    index_text << %Q{  add_index "#{ar_table_name}", ["#{index.columns.map{|c| c.rails_name}*'", "'}"], name: :#{index_name}#{
      unique ? ", unique: true" : ''
    }}
  end

  puts columns.join("\n")
  puts "  end\n\n"

  puts index_text.join("\n")
  puts "\n" unless index_text.empty?
end

#sorted_columns(table, pk, fk_columns) ⇒ Object

We sort the columns here, not in the rmap layer, because it affects the ordering of columns in an index :-(.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/activefacts/generators/rails/schema.rb', line 52

def sorted_columns table, pk, fk_columns
  table.columns.sort_by do |column|
    [ # Emit columns alphabetically, but PK first, then FKs, then others
      case
      when i = pk.index(column)
        i
      when fk_columns.include?(column)
        pk.size+1
      else
        pk.size+2
      end,
      column.rails_name
    ]
  end
end