activefacts-generators 1.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +30 -0
  8. data/Rakefile +6 -0
  9. data/activefacts-generators.gemspec +26 -0
  10. data/lib/activefacts/dependency_analyser.rb +182 -0
  11. data/lib/activefacts/generators/absorption.rb +71 -0
  12. data/lib/activefacts/generators/composition.rb +119 -0
  13. data/lib/activefacts/generators/cql.rb +715 -0
  14. data/lib/activefacts/generators/diagrams/json.rb +340 -0
  15. data/lib/activefacts/generators/help.rb +64 -0
  16. data/lib/activefacts/generators/helpers/inject.rb +16 -0
  17. data/lib/activefacts/generators/helpers/oo.rb +162 -0
  18. data/lib/activefacts/generators/helpers/ordered.rb +605 -0
  19. data/lib/activefacts/generators/helpers/rails.rb +57 -0
  20. data/lib/activefacts/generators/html/glossary.rb +462 -0
  21. data/lib/activefacts/generators/metadata/json.rb +204 -0
  22. data/lib/activefacts/generators/null.rb +32 -0
  23. data/lib/activefacts/generators/rails/models.rb +247 -0
  24. data/lib/activefacts/generators/rails/schema.rb +217 -0
  25. data/lib/activefacts/generators/ruby.rb +134 -0
  26. data/lib/activefacts/generators/sql/mysql.rb +281 -0
  27. data/lib/activefacts/generators/sql/server.rb +274 -0
  28. data/lib/activefacts/generators/stats.rb +70 -0
  29. data/lib/activefacts/generators/text.rb +29 -0
  30. data/lib/activefacts/generators/traits/datavault.rb +241 -0
  31. data/lib/activefacts/generators/traits/oo.rb +73 -0
  32. data/lib/activefacts/generators/traits/ordered.rb +33 -0
  33. data/lib/activefacts/generators/traits/ruby.rb +210 -0
  34. data/lib/activefacts/generators/transform/datavault.rb +303 -0
  35. data/lib/activefacts/generators/transform/surrogate.rb +215 -0
  36. data/lib/activefacts/registry.rb +11 -0
  37. metadata +176 -0
@@ -0,0 +1,217 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ # Generate a Rails-friendly schema.rb from an ActiveFacts vocabulary.
4
+ #
5
+ # Copyright (c) 2012 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ require 'activefacts/metamodel'
8
+ require 'activefacts/rmap'
9
+ require 'activefacts/generators/traits/rails'
10
+ require 'activefacts/registry'
11
+
12
+ module ActiveFacts
13
+ module Generators
14
+ module Rails
15
+ # Generate a Rails-friendly schema for the vocabulary
16
+ # Invoke as
17
+ # afgen --rails/schema[=options] <file>.cql
18
+ class SchemaRb
19
+ private
20
+ include RMap
21
+
22
+ def initialize(vocabulary, *options)
23
+ @vocabulary = vocabulary
24
+ @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
25
+ help if options.include? "help"
26
+ @exclude_fks = options.include? "exclude_fks"
27
+ @include_comments = options.include? "include_comments"
28
+ @closed_world = options.include? "closed_world"
29
+ end
30
+
31
+ def help
32
+ @helping = true
33
+ warn %Q{Options for --rails/schema:
34
+ exclude_fks Don't generate foreign key definitions for use with Rails 4 or the foreigner gem
35
+ include_comments Generate a comment for each column showing the absorption path
36
+ closed_world Set this if your DBMS only allows one null in a unique index (MS SQL)
37
+ }
38
+ end
39
+
40
+ def warn *a
41
+ $stderr.puts *a
42
+ end
43
+
44
+ def puts s
45
+ @out.puts s
46
+ end
47
+
48
+ public
49
+
50
+ # We sort the columns here, not in the rmap layer, because it affects
51
+ # the ordering of columns in an index :-(.
52
+ def sorted_columns table, pk, fk_columns
53
+ table.columns.sort_by do |column|
54
+ [ # Emit columns alphabetically, but PK first, then FKs, then others
55
+ case
56
+ when i = pk.index(column)
57
+ i
58
+ when fk_columns.include?(column)
59
+ pk.size+1
60
+ else
61
+ pk.size+2
62
+ end,
63
+ column.rails_name
64
+ ]
65
+ end
66
+ end
67
+
68
+ def generate_column table, pk, column
69
+ name = column.rails_name
70
+ type, params, constraints = *column.type
71
+ length = params[:length]
72
+ length &&= length.to_i
73
+ scale = params[:scale]
74
+ scale &&= scale.to_i
75
+ rails_type, length = *column.rails_type
76
+
77
+ length_name = rails_type == 'decimal' ? 'precision' : 'limit'
78
+ length_option = length ? ", :#{length_name} => #{length}" : ''
79
+ scale_option = scale ? ", :scale => #{scale}" : ''
80
+
81
+ comment = column.comment
82
+ null_option = ", :null => #{!column.is_mandatory}"
83
+ if pk.size == 1 && pk[0] == column
84
+ case rails_type
85
+ when 'serial'
86
+ rails_type = "primary_key"
87
+ when 'uuid'
88
+ rails_type = "uuid, :default => 'gen_random_uuid()', :primary_key => true"
89
+ end
90
+ else
91
+ case rails_type
92
+ when 'serial'
93
+ rails_type = 'integer' # An integer foreign key
94
+ end
95
+ end
96
+
97
+ (@include_comments ? [" \# #{comment}"] : []) +
98
+ [
99
+ %Q{ t.column "#{name}", :#{rails_type}#{length_option}#{scale_option}#{null_option}}
100
+ ]
101
+ end
102
+
103
+ def generate_columns table, pk, fk_columns
104
+ sc = sorted_columns(table, pk, fk_columns)
105
+ lines = sc.map do |column|
106
+ generate_column table, pk, column
107
+ end
108
+ lines.flatten
109
+ end
110
+
111
+ def generate_table table, foreign_keys
112
+ ar_table_name = table.rails_name
113
+
114
+ pk = table.identifier_columns
115
+ if pk[0].is_auto_assigned
116
+ identity_column = pk[0]
117
+ warn "Warning: redundant column(s) after #{identity_column.name} in primary key of #{ar_table_name}" if pk.size > 1
118
+ end
119
+
120
+ # Get the list of references that give rise to foreign keys:
121
+ fk_refs = table.references_from.select{|ref| ref.is_simple_reference }
122
+
123
+ # Get the list of columns that embody the foreign keys:
124
+ fk_columns = table.columns.select do |column|
125
+ column.references[0].is_simple_reference
126
+ end
127
+
128
+ # Detect if this table is a join table.
129
+ # Join tables have multi-part primary keys that are made up only of foreign keys
130
+ is_join_table = pk.length > 1 and
131
+ !pk.detect do |pk_column|
132
+ !fk_columns.include?(pk_column)
133
+ end
134
+ warn "Warning: #{table.name} has a multi-part primary key" if pk.length > 1 and !is_join_table
135
+
136
+ puts %Q{ create_table "#{ar_table_name}", :id => false, :force => true do |t|}
137
+
138
+ columns = generate_columns table, pk, fk_columns
139
+
140
+ unless @exclude_fks
141
+ table.foreign_keys.each do |fk|
142
+ from_columns = fk.from_columns.map{|column| column.rails_name}
143
+ to_columns = fk.to_columns.map{|column| column.rails_name}
144
+
145
+ foreign_keys.concat(
146
+ if (from_columns.length == 1)
147
+ index_name = RMap.rails_name_trunc('index_'+fk.from.rails_name+'_on_'+from_columns[0])
148
+ [
149
+ " add_foreign_key :#{fk.from.rails_name}, :#{fk.to.rails_name}, :column => :#{from_columns[0]}, :primary_key => :#{to_columns[0]}, :on_delete => :cascade"
150
+ ]+
151
+ Array(
152
+ # Index it non-uniquely only if it's not unique already:
153
+ fk.jump_reference.to_role.unique ? nil :
154
+ " add_index :#{fk.from.rails_name}, [:#{from_columns[0]}], :unique => false, :name => :#{index_name}"
155
+ )
156
+ else
157
+ # This probably isn't going to work without Dr Nic's CPK gem:
158
+ [
159
+ " add_foreign_key :#{fk.to.rails_name}, :#{fk.from.rails_name}, :column => [:#{from_columns.join(':, ')}], :primary_key => [:#{to_columns.join(':, ')}], :on_delete => :cascade"
160
+ ]
161
+ end
162
+ )
163
+ end
164
+ end
165
+
166
+ indices = table.indices
167
+ index_text = []
168
+ indices.each do |index|
169
+ next if index.is_primary && index.columns.size == 1 # We've handled this already
170
+
171
+ index_name = index.rails_name
172
+
173
+ unique = !index.columns.detect{|column| !column.is_mandatory} and !@closed_world
174
+ index_text << %Q{ add_index "#{ar_table_name}", ["#{index.columns.map{|c| c.rails_name}*'", "'}"], :name => :#{index_name}#{
175
+ unique ? ", :unique => true" : ''
176
+ }}
177
+ end
178
+
179
+ puts columns.join("\n")
180
+ puts " end\n\n"
181
+
182
+ puts index_text.join("\n")
183
+ puts "\n" unless index_text.empty?
184
+ end
185
+
186
+ def generate(out = $>) #:nodoc:
187
+ return if @helping
188
+ @out = out
189
+
190
+ foreign_keys = []
191
+
192
+ # If we get index names that need to be truncated, add a counter to ensure uniqueness
193
+ dup_id = 0
194
+
195
+ puts "#\n# schema.rb auto-generated using ActiveFacts for #{@vocabulary.name} on #{Date.today}\n#\n\n"
196
+ puts "ActiveRecord::Base.logger = Logger.new(STDOUT)\n"
197
+ puts "ActiveRecord::Schema.define(:version => #{Time.now.strftime('%Y%m%d%H%M%S')}) do"
198
+ puts " enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')\n"
199
+
200
+ @vocabulary.tables.each do |table|
201
+ generate_table table, foreign_keys
202
+ end
203
+
204
+ unless @exclude_fks
205
+ puts ' unless ENV["EXCLUDE_FKS"]'
206
+ puts foreign_keys.join("\n")
207
+ puts ' end'
208
+ end
209
+ puts "end"
210
+ end
211
+
212
+ end
213
+ end
214
+ end
215
+ end
216
+
217
+ ActiveFacts::Registry.generator('rails/schema', ActiveFacts::Generators::Rails::SchemaRb)
@@ -0,0 +1,134 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ # Generate Ruby classes for the ActiveFacts API from an ActiveFacts vocabulary.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ require 'activefacts/metamodel'
8
+ require 'activefacts/generators/helpers/oo'
9
+ require 'activefacts/generators/traits/ruby'
10
+ # require 'activefacts/generators/traits/rails'
11
+ require 'activefacts/registry'
12
+
13
+ module ActiveFacts
14
+ module Generators
15
+
16
+ # Generate Ruby module containing classes for an ActiveFacts vocabulary.
17
+ # Invoke as
18
+ # afgen --ruby[=options] <file>.cql
19
+ # Options are comma or space separated:
20
+ # * help list available options
21
+ # * sql Emit the sql mapping for tables/columns (REVISIT: not functional at present)
22
+ class RUBY < Helpers::OO
23
+ private
24
+
25
+ def set_option(option)
26
+ @mapping = false
27
+ case option
28
+ when 'help', '?'
29
+ $stderr.puts "Usage:\t\tafgen --ruby[=option,option] input_file.cql\n"+
30
+ "\t\tmapping={sql|rails}\tEmit data to enable mappings to SQL or to Rails"
31
+ exit 0
32
+ when /mapping=(.*)/
33
+ @mapping = $1
34
+ @vocabulary.tables
35
+ else super
36
+ end
37
+ end
38
+
39
+ def vocabulary_start
40
+ puts @vocabulary.prelude
41
+ end
42
+
43
+ def vocabulary_end
44
+ puts @vocabulary.finale
45
+ end
46
+
47
+ def emit_mapping o
48
+ return
49
+ case @mapping
50
+ when 'sql'
51
+ puts " table"
52
+ when 'rails'
53
+ puts " table :#{o.rails_name}"
54
+ end
55
+ end
56
+
57
+ def data_type_dump(o)
58
+ value_type_dump(o, o.name, {}) if o.all_role.size > 0
59
+ end
60
+
61
+ def value_type_dump(o, super_type_name, facets)
62
+ puts o.ruby_definition
63
+ end
64
+
65
+ def subtype_dump(o, supertypes, pi = nil)
66
+ primary_supertype = o && (o.identifying_supertype || o.supertypes[0])
67
+ secondary_supertypes = o.supertypes-[primary_supertype]
68
+
69
+ puts " class #{o.name.gsub(/ /,'')} < #{ primary_supertype.name.gsub(/ /,'') }"
70
+ puts " identified_by #{identified_by(o, pi)}" if pi
71
+ puts " supertypes "+secondary_supertypes.map{|st| st.name.gsub(/ /,'')}*", " if secondary_supertypes.size > 0
72
+ emit_mapping(o) if @mapping && o.is_table
73
+ fact_roles_dump(o.fact_type) if o.fact_type
74
+ roles_dump(o)
75
+ puts " end\n\n"
76
+ pi.ordered_dumped! if pi
77
+ end
78
+
79
+ def non_subtype_dump(o, pi)
80
+ puts " class #{o.name.gsub(/ /,'')}"
81
+
82
+ # We want to name the absorption role only when it's absorbed along its single identifying role.
83
+ puts " identified_by #{identified_by(o, pi)}"
84
+ emit_mapping o if @mapping && o.is_table
85
+ fact_roles_dump(o.fact_type) if o.fact_type
86
+ roles_dump(o)
87
+ puts " end\n\n"
88
+ pi.ordered_dumped!
89
+ end
90
+
91
+ # Dump one fact type.
92
+ def fact_type_dump(fact_type, name)
93
+ return if skip_fact_type(fact_type)
94
+ o = fact_type.entity_type
95
+
96
+ primary_supertype = o && (o.identifying_supertype || o.supertypes[0])
97
+ secondary_supertypes = o.supertypes-[primary_supertype]
98
+
99
+ # Get the preferred identifier, but don't emit it unless it's different from the primary supertype's:
100
+ pi = o.preferred_identifier
101
+ pi = nil if pi && primary_supertype && primary_supertype.preferred_identifier == pi
102
+
103
+ puts " class #{name.gsub(/ /,'')}" +
104
+ (primary_supertype ? " < "+primary_supertype.name.gsub(/ /,'') : "") +
105
+ "\n" +
106
+ secondary_supertypes.map{|sst| " supertype :#{sst.name.gsub(/ /,'_')}"}*"\n" +
107
+ (pi ? " identified_by #{identified_by(o, pi)}" : "")
108
+ emit_mapping o if @mapping && o.is_table
109
+ fact_roles_dump(fact_type)
110
+ roles_dump(o)
111
+ puts " end\n\n"
112
+
113
+ fact_type.ordered_dumped!
114
+ end
115
+
116
+ def identified_by_roles_and_facts(entity_type, identifying_role_refs, identifying_facts)
117
+ identifying_role_refs.map{|role_ref|
118
+ ":"+role_ref.role.preferred_role_name(entity_type)
119
+ }*", "
120
+ end
121
+
122
+ def unary_dump(role, role_name)
123
+ puts " maybe :"+role_name
124
+ end
125
+
126
+ def binary_dump(role, role_name, role_player, mandatory = nil, one_to_one = nil, readings = nil, counterpart_role_name = nil, counterpart_method_name = nil)
127
+ puts role.as_binary(role_name, role_player, mandatory, one_to_one, readings, counterpart_role_name, counterpart_method_name)
128
+ end
129
+
130
+ end
131
+ end
132
+ end
133
+
134
+ ActiveFacts::Registry.generator('ruby', ActiveFacts::Generators::RUBY)
@@ -0,0 +1,281 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ # Generate SQL for MySQL from an ActiveFacts vocabulary.
4
+ #
5
+ # Copyright (c) 2009 Daniel Heath. Read the LICENSE file.
6
+ #
7
+ require 'activefacts/metamodel'
8
+ require 'activefacts/rmap'
9
+ require 'activefacts/registry'
10
+
11
+ module ActiveFacts
12
+ module Generators
13
+ module SQL #:nodoc:
14
+ # Generate SQL for MySQL for an ActiveFacts vocabulary.
15
+ # Invoke as
16
+ # afgen --sql/mysql[=options] <file>.cql
17
+ # Options are comma or space separated:
18
+ # * delay_fks Leave all foreign keys until the end, not just those that contain forward-references
19
+ class MYSQL
20
+ private
21
+ include RMap
22
+ ColumnNameMax = 63
23
+ DefaultCharColLength = 63
24
+
25
+ RESERVED_WORDS = %w{
26
+ ACCESSIBLE ADD ALL ALTER ANALYZE AND AS ASC ASENSITIVE
27
+ BEFORE BETWEEN BIGINT BINARY BLOB BOTH BY CALL CASCADE
28
+ CASE CHANGE CHAR CHARACTER CHECK COLLATE COLUMN CONNECTION
29
+ CONDITION CONSTRAINT CONTINUE CONVERT CREATE CROSS
30
+ CURRENT_DATE CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER
31
+ CURSOR DATABASE DATABASES DAY_HOUR DAY_MICROSECOND
32
+ DAY_MINUTE DAY_SECOND DEC DECIMAL DECLARE DEFAULT DELAYED
33
+ DELETE DESC DESCRIBE DETERMINISTIC DISTINCT DISTINCTROW
34
+ DIV DOUBLE DROP DUAL EACH ELSE ELSEIF ENCLOSED ESCAPED
35
+ EXISTS EXIT EXPLAIN FALSE FETCH FLOAT FLOAT4 FLOAT8 FOR
36
+ FORCE FOREIGN FROM FULLTEXT GRANT GROUP HAVING HIGH_PRIORITY
37
+ HOUR_MICROSECOND HOUR_MINUTE HOUR_SECOND IF IGNORE IN
38
+ INDEX INFILE INNER INOUT INSENSITIVE INSERT INT INT1 INT2
39
+ INT3 INT4 INT8 INTEGER INTERVAL INTO IS ITERATE JOIN KEY
40
+ KEYS KILL LEADING LEAVE LEFT LIKE LIMIT LINEAR LINES LOAD
41
+ LOCALTIME LOCALTIMESTAMP LOCK LONG LONGBLOB LONGTEXT LOOP
42
+ LOW_PRIORITY MASTER_SSL_VERIFY_SERVER_CERT MATCH MEDIUMBLOB
43
+ MEDIUMINT MEDIUMTEXT MIDDLEINT MINUTE_MICROSECOND
44
+ MINUTE_SECOND MOD MODIFIES NATURAL NOT NO_WRITE_TO_BINLOG
45
+ NULL NUMERIC ON OPTIMIZE OPTION OPTIONALLY OR ORDER OUT
46
+ OUTER OUTFILE PRECISION PRIMARY PROCEDURE PURGE RANGE
47
+ READ READ_ONLY READS READ_WRITE READ_WRITE REAL REFERENCES
48
+ REGEXP RELEASE RENAME REPEAT REPLACE REQUIRE RESTRICT
49
+ RETURN REVOKE RIGHT RLIKE SCHEMA SCHEMAS SECOND_MICROSECOND
50
+ SELECT SENSITIVE SEPARATOR SET SHOW SMALLINT SPATIAL
51
+ SPECIFIC SQL SQL_BIG_RESULT SQL_CALC_FOUND_ROWS SQLEXCEPTION
52
+ SQL_SMALL_RESULT SQLSTATE SQLWARNING SSL STARTING
53
+ STRAIGHT_JOIN TABLE TERMINATED THEN TINYBLOB TINYINT
54
+ TINYTEXT TO TRAILING TRIGGER TRUE UNDO UNION UNIQUE UNLOCK
55
+ UNSIGNED UPDATE UPGRADE USAGE USE USING UTC_DATE UTC_TIME
56
+ UTC_TIMESTAMP VALUES VARBINARY VARCHAR VARCHARACTER VARYING
57
+ WHEN WHERE WHILE WITH WRITE XOR YEAR_MONTH ZEROFILL
58
+ }.inject({}){ |h,w| h[w] = true; h }
59
+
60
+ def initialize(vocabulary, *options)
61
+ @vocabulary = vocabulary
62
+ @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
63
+ @delay_fks = options.include? "delay_fks"
64
+ end
65
+
66
+ def puts s
67
+ @out.puts s
68
+ end
69
+
70
+ def go s
71
+ puts s + ";\n\n"
72
+ end
73
+
74
+ def escape s
75
+ # Escape SQL keywords and non-identifiers
76
+ s = s[0...120]
77
+ if s =~ /[^A-Za-z0-9_]/ || RESERVED_WORDS[s.upcase]
78
+ "`#{s}`"
79
+ else
80
+ s
81
+ end
82
+ end
83
+
84
+ # Return SQL type and (modified?) length for the passed base type
85
+ def normalise_type(type, length)
86
+ sql_type = case type
87
+ when /^Auto ?Counter$/
88
+ 'int'
89
+
90
+ when /^Signed ?Integer$/,
91
+ /^Signed ?Small ?Integer$/
92
+ s = case
93
+ when length <= 8
94
+ 'tinyint'
95
+ when length <= 16
96
+ 'shortint'
97
+ when length <= 32
98
+ 'int'
99
+ else 'bigint'
100
+ end
101
+ length = nil
102
+ s
103
+
104
+ when /^Unsigned ?Integer$/,
105
+ /^Unsigned ?Small ?Integer$/,
106
+ /^Unsigned ?Tiny ?Integer$/
107
+ s = case
108
+ when length <= 8
109
+ 'tinyint unsigned'
110
+ when length <= 16
111
+ 'shortint unsigned'
112
+ when length <= 32
113
+ 'int unsigned'
114
+ else 'bigint'
115
+ end
116
+ length = nil
117
+ s
118
+
119
+ when /^Decimal$/
120
+ 'decimal'
121
+
122
+ when /^Fixed ?Length ?Text$/, /^Char$/
123
+ length ||= DefaultCharColLength
124
+ "char"
125
+ when /^Variable ?Length ?Text$/, /^String$/
126
+ length ||= DefaultCharColLength
127
+ "varchar"
128
+ # There are several large length text types; If you need to store more than 65k chars, look at using MEDIUMTEXT or LONGTEXT
129
+ # CQL does not yet allow you to specify a length for LargeLengthText.
130
+ when /^Large ?Length ?Text$/, /^Text$/
131
+ 'text'
132
+
133
+ when /^Date ?And ?Time$/, /^Date ?Time$/
134
+ 'datetime'
135
+ when /^Date$/
136
+ 'date'
137
+ when /^Time$/
138
+ 'time'
139
+ when /^Auto ?Time ?Stamp$/
140
+ 'timestamp'
141
+
142
+ when /^Money$/
143
+ 'decimal'
144
+ # Warning: Max 65 kbytes. To use larger types, try MediumBlob (16mb) or LongBlob (4gb)
145
+ when /^Picture ?Raw ?Data$/, /^Image$/
146
+ 'blob'
147
+ when /^Variable ?Length ?Raw ?Data$/, /^Blob$/
148
+ 'blob'
149
+ # Assuming you only want a boolean out of this. Should we specify length instead?
150
+ when /^BIT$/
151
+ 'bit'
152
+ else type # raise "SQL type unknown for standard type #{type}"
153
+ end
154
+ [sql_type, length]
155
+ end
156
+
157
+ public
158
+ def generate(out = $>) #:nodoc:
159
+ @out = out
160
+ #go "CREATE SCHEMA #{@vocabulary.name}"
161
+
162
+ tables_emitted = {}
163
+ delayed_foreign_keys = []
164
+
165
+ @vocabulary.tables.each do |table|
166
+ puts "CREATE TABLE #{escape table.name.gsub(' ','')} ("
167
+
168
+ pk = table.identifier_columns
169
+ identity_column = pk[0] if pk[0].is_auto_assigned
170
+
171
+ fk_refs = table.references_from.select{|ref| ref.is_simple_reference }
172
+ fk_columns = table.columns.select do |column|
173
+ column.references[0].is_simple_reference
174
+ end
175
+
176
+ # We sort the columns here, not in the rmap layer, because it affects
177
+ # the ordering of columns in an index :-(.
178
+ columns = table.columns.sort_by { |column| column.name(nil) }.map do |column|
179
+ name = escape column.name("")
180
+ padding = " "*(name.size >= ColumnNameMax ? 1 : ColumnNameMax-name.size)
181
+ type, params, constraints = column.type
182
+ constraints = [] if (fk_columns.include?(column)) # Don't enforce VT constraints on FK columns
183
+ length = params[:length]
184
+ length &&= length.to_i
185
+ scale = params[:scale]
186
+ scale &&= scale.to_i
187
+ type, length = normalise_type(type, length)
188
+ sql_type = "#{type}#{
189
+ if !length
190
+ ""
191
+ else
192
+ "(" + length.to_s + (scale ? ", #{scale}" : "") + ")"
193
+ end
194
+ }"
195
+ identity = column == identity_column ? " AUTO_INCREMENT" : ""
196
+ null = (column.is_mandatory ? "NOT " : "") + "NULL"
197
+ check = check_clause(name, constraints)
198
+ comment = column.comment
199
+ [ "-- #{comment}", "#{name}#{padding}#{sql_type}#{identity} #{null}#{check}" ]
200
+ end.flatten
201
+
202
+ pk_def = (pk.detect{|column| !column.is_mandatory} ? "UNIQUE(" : "PRIMARY KEY(") +
203
+ pk.map{|column| escape column.name("")}*", " +
204
+ ")"
205
+
206
+ inline_fks = []
207
+ table.foreign_keys.each do |fk|
208
+ fk_text = "FOREIGN KEY (" +
209
+ fk.from_columns.map{|column| column.name}*", " +
210
+ ") REFERENCES #{escape fk.to.name.gsub(' ','')} (" +
211
+ fk.to_columns.map{|column| column.name}*", " +
212
+ ")"
213
+ if !@delay_fks and # We don't want to delay all Fks
214
+ (tables_emitted[fk.to] or # The target table has been emitted
215
+ fk.to == table && !fk.to_columns.detect{|column| !column.is_mandatory}) # The reference columns already have the required indexes
216
+ inline_fks << fk_text
217
+ else
218
+ delayed_foreign_keys << ("ALTER TABLE #{escape fk.from.name.gsub(' ','')}\n\tADD " + fk_text)
219
+ end
220
+ end
221
+
222
+ indices = table.indices
223
+ inline_indices = []
224
+ delayed_indices = []
225
+ indices.each do |index|
226
+ next if index.over == table && index.is_primary # Already did the primary keys
227
+ abbreviated_column_names = index.abbreviated_column_names*""
228
+ column_names = index.column_names
229
+ column_name_list = column_names.map{|n| escape(n)}*", "
230
+ inline_indices << "UNIQUE(#{column_name_list})"
231
+ end
232
+
233
+ tables_emitted[table] = true
234
+
235
+ puts("\t" + (columns + [pk_def] + inline_indices + inline_fks)*",\n\t")
236
+ go ")"
237
+ delayed_indices.each {|index_text|
238
+ go index_text
239
+ }
240
+ end
241
+
242
+ delayed_foreign_keys.each do |fk|
243
+ go fk
244
+ end
245
+ end
246
+
247
+ private
248
+ def sql_value(value)
249
+ value.is_literal_string ? sql_string(value.literal) : value.literal
250
+ end
251
+
252
+ def sql_string(str)
253
+ "'" + str.gsub(/'/,"''") + "'"
254
+ end
255
+
256
+ def check_clause(column_name, constraints)
257
+ return "" if constraints.empty?
258
+ # REVISIT: Merge all constraints (later; now just use the first)
259
+ " CHECK(" +
260
+ constraints[0].all_allowed_range_sorted.map do |ar|
261
+ vr = ar.value_range
262
+ min = vr.minimum_bound
263
+ max = vr.maximum_bound
264
+ if (min && max && max.value.literal == min.value.literal)
265
+ "#{column_name} = #{sql_value(min.value)}"
266
+ else
267
+ inequalities = [
268
+ min && "#{column_name} >#{min.is_inclusive ? "=" : ""} #{sql_value(min.value)}",
269
+ max && "#{column_name} <#{max.is_inclusive ? "=" : ""} #{sql_value(max.value)}"
270
+ ].compact
271
+ inequalities.size > 1 ? "(" + inequalities*" AND " + ")" : inequalities[0]
272
+ end
273
+ end*" OR " +
274
+ ")"
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end
280
+
281
+ ActiveFacts::Registry.generator('sql/mysql', ActiveFacts::Generators::SQL::MYSQL)