dataMetaDom 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,295 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
2
+
3
+ %w(fileutils set).each { |r| require r }
4
+
5
+ module DataMetaDom
6
+
7
+ =begin rdoc
8
+ Definition for generating Oracle 11 and later artifacts such as schemas, select statements,
9
+ ORM input files etc etc
10
+
11
+ TODO this isn't a bad way, but beter use templating next time such as {ERB}[http://ruby-doc.org/stdlib-1.9.3/libdoc/erb/rdoc/ERB.html].
12
+
13
+ For command line details either check the new method's source or the README.rdoc file, the usage section.
14
+ =end
15
+ module OraLexer
16
+
17
+ =begin rdoc
18
+ Integer types
19
+ =end
20
+ INT_TYPES = {2 => 'number(6)', 4 => 'number(9)', 8 => 'number(38)'}
21
+
22
+ =begin rdoc
23
+ Not null (required) wording per Oracle DDL syntax
24
+ =end
25
+ NOT_NULL=' not null'
26
+
27
+ =begin rdoc
28
+ \Mapping from DataMeta DOM standard types to correspondent Oracle types renderer lambdas.
29
+ =end
30
+ SQL_TYPES={
31
+ INT => lambda { |len, scale, isReq|
32
+ concreteType = INT_TYPES[len]
33
+ raise "Invalid integer type length #{len} " unless concreteType
34
+ "#{concreteType}#{isReq ? NOT_NULL : ''}"
35
+ },
36
+ STRING => lambda { |len, scale, isReq| "nvarchar2(#{len})#{isReq ? NOT_NULL : ''}" },
37
+ DATETIME => lambda { |len, scale, isReq| "timestamp#{isReq ? NOT_NULL : ''}" },
38
+ # Boolean implementation in Oracle is not optimal, see the doc:
39
+ # http://docs.oracle.com/cd/B19306_01/olap.102/b14346/dml_datatypes004.htm
40
+ BOOL => lambda { |len, scale, isReq| "boolean#{isReq ? NOT_NULL : ''}" },
41
+ CHAR => ->(len, scale, isReq) { "nchar(#{len})#{isReq ? NOT_NULL : ''}" },
42
+ NUMERIC => ->(len, scale, isReq) { "number(#{len}, #{scale})#{isReq ? NOT_NULL : ''}" }
43
+
44
+ }
45
+
46
+ =begin rdoc
47
+ Encapsulates 4 parts of DDL related SQL output:
48
+ * Creates
49
+ * Drops
50
+ * Linking aka Coupling aka creating Foreign Keys
51
+ * Unlinking aka Uncoupling aka dropping Foreign Keys
52
+ =end
53
+ class SqlOutput
54
+
55
+ =begin rdoc
56
+ Open output file into create SQL DDL statements (CREATE TABLE)
57
+ =end
58
+ attr_reader :create
59
+
60
+ =begin rdoc
61
+ Open output file into drop SQL DDL statements (DROP TABLE)
62
+ =end
63
+ attr_reader :drop
64
+
65
+ =begin rdoc
66
+ Open output file into the \couple SQL DDL statements, creating foreign keys
67
+ =end
68
+ attr_reader :couple
69
+ =begin rdoc
70
+ Open output file into the \uncouple SQL DDL statements, dropping foreign keys
71
+ =end
72
+ attr_reader :uncouple
73
+ =begin rdoc
74
+ Sequences and triggers - create
75
+ =end
76
+ attr_reader :crSeqs
77
+ =begin rdoc
78
+ Sequences and triggers - drop
79
+ =end
80
+ attr_reader :drSeqs
81
+ =begin rdoc
82
+ Indexes
83
+ =end
84
+ attr_reader :ixes
85
+
86
+
87
+ =begin rdoc
88
+ Creates an instance into the given target directory in which all 4 parts of the SQL DDL
89
+ process will be created.
90
+ =end
91
+ def initialize(sqlTargetDir)
92
+ @selTargetDir = sqlTargetDir
93
+ @create = File.new("#{sqlTargetDir}/DDL-createTables.sql", 'wb')
94
+ @crSeqs = File.new("#{sqlTargetDir}/DDL-createSeqs.sql", 'wb')
95
+ @drSeqs = File.new("#{sqlTargetDir}/DDL-dropSeqs.sql", 'wb')
96
+ @ixes = File.new("#{sqlTargetDir}/DDL-indexes.sql", 'wb')
97
+ @drop = File.new("#{sqlTargetDir}/DDL-drop.sql", 'wb')
98
+ @couple = File.new("#{sqlTargetDir}/DDL-couple.sql", 'wb')
99
+ @uncouple = File.new("#{sqlTargetDir}/DDL-uncouple.sql", 'wb')
100
+ @allScriptFiles = [@create, @drop, @couple, @uncouple, @drSeqs, @crSeqs, @ixes]
101
+ @dropScripts = [@uncouple, @drop]
102
+ @allScriptFiles.each { |f|
103
+ f.puts %q</* Generated by DataMeta DOM Oracle utility
104
+ DO NOT EDIT MANUALLY, update the DataMeta DOM source and regen.
105
+ */
106
+ >
107
+ }
108
+ @dropScripts.each { |ds|
109
+ ds.puts %q<
110
+ /* Oracle does not have this feature: Disable all checks for safe dropping without any errors */
111
+
112
+ >
113
+ }
114
+ end
115
+
116
+ =begin rdoc
117
+ Safely closes all the output files.
118
+ =end
119
+ def close
120
+ @dropScripts.each { |ds|
121
+ ds.puts %q<
122
+
123
+ /* Placeholder for a drop footer */
124
+ >
125
+ }
126
+ @allScriptFiles.each { |f|
127
+ begin
128
+ f.close
129
+ rescue Exception => x;
130
+ $stderr.puts x.message
131
+ end
132
+ }
133
+ end
134
+ end
135
+
136
+ =begin rdoc
137
+ Renders autoincrement the very special Oracle way, via a sequence
138
+ FIXME: need to check the auto hint
139
+
140
+ @return empty string
141
+ =end
142
+ def autoGenClauseIfAny(out, record, field)
143
+ if record.identity && record.identity.length == 1 && field.name == record.identity[0] &&
144
+ field.dataType.type == DataMetaDom::INT
145
+ ns, entityName = DataMetaDom.splitNameSpace record.name
146
+ seqName = "#{entityName}_#{field.name}_sq"
147
+ # Transaction separators are important for triggers and sequences
148
+ out.crSeqs.puts %|
149
+ CREATE SEQUENCE #{seqName};
150
+
151
+ /
152
+ |
153
+ =begin rdoc
154
+ To make the sequence used automatically:
155
+ CREATE OR REPLACE TRIGGER #{entityName}_#{field.name}_trg
156
+ BEFORE INSERT ON #{entityName}
157
+ FOR EACH ROW
158
+
159
+ BEGIN
160
+ SELECT #{seqName}.NEXTVAL
161
+ INTO :new.#{field.name}
162
+ FROM dual;
163
+ END;
164
+ =end
165
+ out.drSeqs.puts %|
166
+
167
+ drop SEQUENCE #{seqName};
168
+ /
169
+ |
170
+ end
171
+
172
+ ''
173
+ end
174
+
175
+ =begin rdoc
176
+ Renders the given field into create statement.
177
+ * Parameters:
178
+ * +createStatement+ - the create statement to append the field definition to.
179
+ * +parser+ - the instance of the Model
180
+ * +record+ - the instance of the Record to which the field belongs
181
+ * +fieldKey+ - the full name of the field to render turned into a symbol.
182
+ * +isFirstField+ - the boolean, true if the field is first in the create statement.
183
+ =end
184
+ def renderField(out, createStatement, parser, record, fieldKey, isFirstField)
185
+ field = record[fieldKey]
186
+ ty = field.dataType
187
+ stdRenderer = SQL_TYPES[ty.type]
188
+ typeEnum = parser.enums[ty.type]
189
+ typeRec = parser.records[ty.type]
190
+
191
+ typeDef = if stdRenderer
192
+ stdRenderer.call ty.length, ty.scale, field.isRequired
193
+ elsif typeEnum
194
+ "enum('#{typeEnum.values.join("','")}')"
195
+ elsif typeRec
196
+ raise "Invalid ref to #{typeRec} - it has no singular ID" unless typeRec.identity.length == 1
197
+ idField = typeRec[typeRec.identity[0]]
198
+ idRenderer = SQL_TYPES[idField.dataType.type]
199
+ raise 'Only one-level prim type references only allowed in this version' unless idRenderer
200
+ idRenderer.call idField.dataType.length, idField.dataType.scale, field.isRequired
201
+ end
202
+ createStatement << ",\n" unless isFirstField
203
+ createStatement << "\t#{field.name} #{typeDef}#{autoGenClauseIfAny(out, record, field)}"
204
+ end
205
+
206
+ =begin rdoc
207
+ Builds and returns the foreign key name for the given entity (Record) name and the counting number of these.
208
+ * Parameters:
209
+ * +bareEntityName+ - the entity name without the namespace
210
+ * +index+ - an integer, an enumerated counting number, starting from one. For each subsequent FK this number is
211
+ incremented.
212
+ =end
213
+ def fkName(bareEntityName, index)
214
+ "fk_#{bareEntityName}_#{index}"
215
+ end
216
+
217
+ =begin rdoc
218
+ Render SQL record with for the given model into the given output.
219
+ * Parameters
220
+ * +out+ - an instance of SqlOutput
221
+ * +parser+ - an instance of Model
222
+ * +recordKey+ - full name of the record datatype including namespeace if any turned into a symbol.
223
+ =end
224
+ def renderRecord(out, parser, recordKey)
225
+ record = parser.records[recordKey]
226
+ ns, entityName = DataMetaDom.splitNameSpace record.name
227
+ isFirstField = true
228
+ # Oracle does not have neatly defined feature of dropping table if it exists
229
+ # https://community.oracle.com/thread/2421779?tstart=0
230
+ out.drop.puts %|\ndrop table #{entityName};
231
+ /
232
+ |
233
+ fkNumber = 1 # to generate unique names that fit in 64 characters of identifier max length for Oracle
234
+ record.refs.select { |r| r.type == Reference::RECORD }.each { |ref|
235
+ ns, fromEntityBareName = DataMetaDom.splitNameSpace ref.fromEntity.name
236
+ ns, toEntityBareName = DataMetaDom.splitNameSpace ref.toEntity.name
237
+ out.couple.puts "alter table #{fromEntityBareName} add constraint #{fkName(fromEntityBareName, fkNumber)} "\
238
+ " foreign key (#{ref.fromField.name}) references #{toEntityBareName}(#{ref.toFields.name});"
239
+ out.uncouple.puts "alter table #{fromEntityBareName} drop foreign key #{fkName(fromEntityBareName, fkNumber)};"
240
+ fkNumber += 1
241
+ }
242
+ ids = record.identity ? record.identity.args : []
243
+ createStatement = "create table #{entityName} (\n"
244
+ fieldKeys = [] << ids.map { |i| i.to_s }.sort.map { |i| i.to_sym } \
245
+ << record.fields.keys.select { |k| !ids.include?(k) }.map { |k| k.to_s }.sort.map { |k| k.to_sym }
246
+
247
+ fieldKeys.flatten.each { |f|
248
+ renderField(out, createStatement, parser, record, f, isFirstField)
249
+ isFirstField = false
250
+ }
251
+ if record.identity && record.identity.length > 0
252
+ createStatement << ",\n\tprimary key(#{ids.sort.join(', ')})"
253
+ end
254
+ unless record.uniques.empty?
255
+ uqNumber = 1
256
+ record.uniques.each_value { |uq|
257
+ createStatement << ",\n\tconstraint uq_#{entityName}_#{uqNumber} unique(#{uq.args.join(', ')})"
258
+ uqNumber += 1 # to generate unique names that fit in 30 characters of identifier max length for Oracle
259
+ }
260
+ end
261
+ unless record.indexes.empty?
262
+ ixNumber = 1
263
+ record.indexes.each_value { |ix|
264
+ out.ixes.puts %|
265
+ CREATE INDEX #{entityName}_#{ixNumber} ON #{entityName}(#{ix.args.join(', ')});
266
+ /
267
+ |
268
+ # createStatement << ",\n\tindex ix_#{entityName}_#{ixNumber}(#{ix.args.join(', ')})"
269
+ ixNumber += 1 # to generate unique names that fit in 64 characters of identifier max length for Oracle
270
+ }
271
+ end
272
+ createStatement << "\n);\n/\n"
273
+
274
+ out.create.puts createStatement
275
+ end
276
+
277
+ =begin rdoc
278
+ Generate the Oracle DDL from the given Model into the given output directory.
279
+ * Parameters
280
+ * +parser+ - an instance of a Model
281
+ * +outDir+ - a String, the directory to generate the DDL into.
282
+ =end
283
+ def genDdl(parser, outDir)
284
+ out = SqlOutput.new(outDir)
285
+ begin
286
+ parser.records.each_key { |r|
287
+ renderRecord(out, parser, r)
288
+ }
289
+ ensure
290
+ out.close
291
+ end
292
+ end
293
+
294
+ end
295
+ end