dataMetaDom 1.0.0

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.
@@ -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