activefacts-generators 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)