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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +30 -0
- data/Rakefile +6 -0
- data/activefacts-generators.gemspec +26 -0
- data/lib/activefacts/dependency_analyser.rb +182 -0
- data/lib/activefacts/generators/absorption.rb +71 -0
- data/lib/activefacts/generators/composition.rb +119 -0
- data/lib/activefacts/generators/cql.rb +715 -0
- data/lib/activefacts/generators/diagrams/json.rb +340 -0
- data/lib/activefacts/generators/help.rb +64 -0
- data/lib/activefacts/generators/helpers/inject.rb +16 -0
- data/lib/activefacts/generators/helpers/oo.rb +162 -0
- data/lib/activefacts/generators/helpers/ordered.rb +605 -0
- data/lib/activefacts/generators/helpers/rails.rb +57 -0
- data/lib/activefacts/generators/html/glossary.rb +462 -0
- data/lib/activefacts/generators/metadata/json.rb +204 -0
- data/lib/activefacts/generators/null.rb +32 -0
- data/lib/activefacts/generators/rails/models.rb +247 -0
- data/lib/activefacts/generators/rails/schema.rb +217 -0
- data/lib/activefacts/generators/ruby.rb +134 -0
- data/lib/activefacts/generators/sql/mysql.rb +281 -0
- data/lib/activefacts/generators/sql/server.rb +274 -0
- data/lib/activefacts/generators/stats.rb +70 -0
- data/lib/activefacts/generators/text.rb +29 -0
- data/lib/activefacts/generators/traits/datavault.rb +241 -0
- data/lib/activefacts/generators/traits/oo.rb +73 -0
- data/lib/activefacts/generators/traits/ordered.rb +33 -0
- data/lib/activefacts/generators/traits/ruby.rb +210 -0
- data/lib/activefacts/generators/transform/datavault.rb +303 -0
- data/lib/activefacts/generators/transform/surrogate.rb +215 -0
- data/lib/activefacts/registry.rb +11 -0
- 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)
|