scheman 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,599 @@
1
+ module Scheman
2
+ module Parsers
3
+ class Mysql < Base
4
+ def self.parser
5
+ @parser ||= ParsletParser.new
6
+ end
7
+
8
+ def self.transform
9
+ @transform ||= ParsletTransform.new
10
+ end
11
+
12
+ # @param schema [String]
13
+ # @return [Scheman::Schema]
14
+ def self.parse(schema)
15
+ Schema.new(
16
+ transform.apply(
17
+ parser.parse(
18
+ schema
19
+ )
20
+ )
21
+ )
22
+ end
23
+
24
+ def parse(schema)
25
+ self.class.parse(schema)
26
+ end
27
+
28
+ class ParsletParser < Parslet::Parser
29
+ # @return [Parslet::Atoms::Sequence] Case-insensitive pattern from a given string
30
+ def case_insensitive_str(str)
31
+ str.each_char.map {|char| match[char.downcase + char.upcase] }.reduce(:>>)
32
+ end
33
+
34
+ # @return [Parslet::Atoms::Repetation]
35
+ def non(sequence)
36
+ (sequence.absent? >> any).repeat
37
+ end
38
+
39
+ def quoted(value)
40
+ single_quoted(value) | double_quoted(value) | back_quoted(value)
41
+ end
42
+
43
+ def quoted_string
44
+ single_quoted(non(str("'"))) | double_quoted(non(str('"'))) | back_quoted(non(str("`")))
45
+ end
46
+
47
+ def single_quoted(value)
48
+ str("'") >> value >> str("'")
49
+ end
50
+
51
+ def double_quoted(value)
52
+ str('"') >> value >> str('"')
53
+ end
54
+
55
+ def back_quoted(value)
56
+ str("`") >> value >> str("`")
57
+ end
58
+
59
+ def parenthetical(value)
60
+ str("(") >> spaces? >> value >> spaces? >> str(")")
61
+ end
62
+
63
+ def comma_separated(value)
64
+ value >> (str(",") >> spaces? >> value).repeat
65
+ end
66
+
67
+ root(:statements)
68
+
69
+ rule(:statements) do
70
+ statement.repeat(1).as(:statements)
71
+ end
72
+
73
+ rule(:statement) do
74
+ comment |
75
+ use |
76
+ set |
77
+ drop |
78
+ create_database |
79
+ create_table.as(:create_table) |
80
+ alter |
81
+ insert |
82
+ delimiter_statement |
83
+ empty_statement
84
+ end
85
+
86
+ rule(:newline) do
87
+ str("\n") >> str("\r").maybe
88
+ end
89
+
90
+ rule(:space) do
91
+ match('\s')
92
+ end
93
+
94
+ rule(:spaces) do
95
+ space.repeat(1)
96
+ end
97
+
98
+ rule(:spaces?) do
99
+ spaces.maybe
100
+ end
101
+
102
+ rule(:delimiter) do
103
+ str(";")
104
+ end
105
+
106
+ rule(:something) do
107
+ spaces >> match('[\S]').repeat >> spaces
108
+ end
109
+
110
+ rule(:string) do
111
+ any.repeat(1)
112
+ end
113
+
114
+ rule(:eol) do
115
+ delimiter >> spaces?
116
+ end
117
+
118
+ rule(:comment) do
119
+ (str("#") | str("--")) >> non(newline) >> newline >> spaces?
120
+ end
121
+
122
+ rule(:use) do
123
+ case_insensitive_str("use") >> spaces >> non(delimiter).as(:database_name) >> eol
124
+ end
125
+
126
+ rule(:set) do
127
+ case_insensitive_str("set") >> non(delimiter) >> eol
128
+ end
129
+
130
+ rule(:drop) do
131
+ case_insensitive_str("drop table") >> non(delimiter) >> eol
132
+ end
133
+
134
+ rule(:create_database) do
135
+ create_database_beginning >> non(delimiter) >> eol
136
+ end
137
+
138
+ rule(:create_database_beginning) do
139
+ case_insensitive_str("create") >> str(" ") >> word_database
140
+ end
141
+
142
+ rule(:word_database) do
143
+ case_insensitive_str("database") | case_insensitive_str("schema")
144
+ end
145
+
146
+ rule(:word_index) do
147
+ case_insensitive_str("key") | case_insensitive_str("index")
148
+ end
149
+
150
+ rule(:create_table) do
151
+ create_table_beginning >> spaces >> table_name >>
152
+ spaces? >> table_components >> eol
153
+ end
154
+
155
+ rule(:table_components) do
156
+ parenthetical(comma_separated(create_definition)).as(:table_components)
157
+ end
158
+
159
+ rule(:table_name) do
160
+ (quoted_identifier | identifier).as(:table_name)
161
+ end
162
+
163
+ rule(:create_table_beginning) do
164
+ case_insensitive_str("create") >> case_insensitive_str(" temporary").maybe >>
165
+ case_insensitive_str(" table") >> case_insensitive_str(" if not exists").maybe
166
+ end
167
+
168
+ rule(:create_definition) do
169
+ index | field | comment
170
+ end
171
+
172
+ rule(:index) do
173
+ (
174
+ primary_key |
175
+ unique_key |
176
+ foreign_key |
177
+ normal_index |
178
+ fulltext_index |
179
+ spatial_index
180
+ ).as(:index)
181
+ end
182
+
183
+ rule(:primary_key) do
184
+ (
185
+ case_insensitive_str("primary key") >>
186
+ optional_index_type >>
187
+ spaces >> parenthetical(comma_separated(column_name_with_optional_values)) >>
188
+ optional_index_type
189
+ ).as(:primary_key)
190
+ end
191
+
192
+ rule(:optional_index_type) do
193
+ (spaces >> index_type).maybe
194
+ end
195
+
196
+ rule(:unique_key) do
197
+ str("TODO")
198
+ end
199
+
200
+ rule(:foreign_key) do
201
+ str("TODO")
202
+ end
203
+
204
+ rule(:normal_index) do
205
+ word_index >> spaces >> index_name >>
206
+ optional_using_index_type >>
207
+ spaces >> parenthetical(comma_separated(column_name_with_optional_values)) >>
208
+ optional_using_index_type
209
+ end
210
+
211
+ rule(:optional_using_index_type) do
212
+ (spaces >> using_index_type).maybe
213
+ end
214
+
215
+ rule(:using_index_type) do
216
+ case_insensitive_str("using") >> spaces >> index_type
217
+ end
218
+
219
+ rule(:index_name) do
220
+ identifier.as(:index_name)
221
+ end
222
+
223
+ # TODO: Fix spaces not to allow no space
224
+ rule(:fulltext_index) do
225
+ (
226
+ case_insensitive_str("fulltext") >> spaces >>
227
+ (word_index >> spaces).maybe >>
228
+ (index_name >> spaces).maybe >>
229
+ parenthetical(comma_separated(column_name_with_optional_values))
230
+ ).as(:fulltext_index)
231
+ end
232
+
233
+ rule(:spatial_index) do
234
+ (
235
+ case_insensitive_str("spatial") >> spaces >>
236
+ (word_index >> spaces).maybe >>
237
+ (index_name >> spaces).maybe >>
238
+ parenthetical(comma_separated(column_name_with_optional_values))
239
+ ).as(:spatial_index)
240
+ end
241
+
242
+ rule(:field) do
243
+ (
244
+ comment.repeat >> field_name.as(:field_name) >> spaces >> field_type >>
245
+ (spaces >> field_qualifiers).maybe.as(:field_qualifiers) >>
246
+ (spaces >> field_comment).maybe >>
247
+ (spaces >> reference_definition).maybe >>
248
+ (spaces >> on_update).maybe >>
249
+ comment.maybe
250
+ ).as(:field)
251
+ end
252
+
253
+ rule(:field_name) do
254
+ quoted_identifier | identifier
255
+ end
256
+
257
+ rule(:field_qualifiers) do
258
+ (field_qualifier >> (spaces >> field_qualifier).repeat)
259
+ end
260
+
261
+ # TODO: default value, on update
262
+ rule(:field_qualifier) do
263
+ not_null_qualifier |
264
+ null_qualifier |
265
+ primary_key_qualifier |
266
+ auto_increment_qualifier |
267
+ character_set_qualifier |
268
+ collate_qualifier |
269
+ unique_key_qualifier |
270
+ key_qualifier
271
+ end
272
+
273
+ rule(:key_qualifier) do
274
+ word_index.as(:key_qualifier)
275
+ end
276
+
277
+ rule(:unique_key_qualifier) do
278
+ (case_insensitive_str("unique ") >> word_index).as(:unique_key_qualifier)
279
+ end
280
+
281
+ rule(:collate_qualifier) do
282
+ (case_insensitive_str("collate") >> spaces >> identifier).as(:collate_qualifier)
283
+ end
284
+
285
+ rule(:character_set_qualifier) do
286
+ (case_insensitive_str("character set") >> spaces >> identifier).as(:character_set_qualifier)
287
+ end
288
+
289
+ rule(:primary_key_qualifier) do
290
+ case_insensitive_str("primary key").as(:primary_key_qualifier)
291
+ end
292
+
293
+ rule(:null_qualifier) do
294
+ case_insensitive_str("null").as(:null_qualifier)
295
+ end
296
+
297
+ rule(:not_null_qualifier) do
298
+ case_insensitive_str("not null").as(:not_null_qualifier)
299
+ end
300
+
301
+ rule(:auto_increment_qualifier) do
302
+ case_insensitive_str("auto increment").as(:auto_increment_qualifier)
303
+ end
304
+
305
+ rule(:field_comment) do
306
+ case_insensitive_str("comment") >> spaces >> single_quoted(match("[^']"))
307
+ end
308
+
309
+ rule(:on_update) do
310
+ str("TODO")
311
+ end
312
+
313
+ rule(:field_type) do
314
+ field_type_name >>
315
+ (spaces? >> parenthetical(comma_separated(field_value))).maybe.as(:field_values) >>
316
+ (spaces >> type_qualifier).repeat
317
+ end
318
+
319
+ rule(:field_value) do
320
+ value.as(:field_value)
321
+ end
322
+
323
+ rule(:field_type_name) do
324
+ identifier.as(:field_type_name)
325
+ end
326
+
327
+ rule(:type_qualifier) do
328
+ case_insensitive_str("binary") | case_insensitive_str("unsigned") | case_insensitive_str("zerofill")
329
+ end
330
+
331
+ rule(:column_name_with_optional_values) do
332
+ column_name >> (spaces? >> parenthetical(comma_separated(value))).maybe
333
+ end
334
+
335
+ rule(:column_name) do
336
+ quoted_identifier.as(:column_name)
337
+ end
338
+
339
+ rule(:value) do
340
+ float_number | quoted_string | str("NULL")
341
+ end
342
+
343
+ rule(:float_number) do
344
+ base_number >> exponent_number.maybe
345
+ end
346
+
347
+ rule(:sign) do
348
+ match("[-+]")
349
+ end
350
+
351
+ rule(:unsigned_integer) do
352
+ match('\d').repeat(1)
353
+ end
354
+
355
+ rule(:base_number) do
356
+ sign.maybe >> str(".").maybe >> unsigned_integer
357
+ end
358
+
359
+ rule(:exponent_number) do
360
+ match("e|E") >> unsigned_integer
361
+ end
362
+
363
+ rule(:index_type) do
364
+ (
365
+ case_insensitive_str("btree") | case_insensitive_str("hash") | case_insensitive_str("rtree")
366
+ ).as(:index_type)
367
+ end
368
+
369
+ rule(:identifier) do
370
+ match('\w').repeat(1).as(:identifier)
371
+ end
372
+
373
+ rule(:quoted_identifier) do
374
+ (
375
+ single_quoted(match("[^']").repeat(1)) |
376
+ double_quoted(match('[^"]').repeat(1)) |
377
+ back_quoted(match("[^`]").repeat(1))
378
+ ).as(:quoted_identifier)
379
+ end
380
+
381
+ rule(:insert) do
382
+ case_insensitive_str("insert") >> non(delimiter) >> eol
383
+ end
384
+
385
+ rule(:alter) do
386
+ case_insensitive_str("alter table") >> something >> comma_separated(alter_specification) >> eol
387
+ end
388
+
389
+ rule(:alter_specification) do
390
+ case_insensitive_str("add") >> spaces >> foreign_key_def
391
+ end
392
+
393
+ rule(:foreign_key_def) do
394
+ foreign_key_def_begin >> spaces >> parens_field_list >> spaces >> reference_definition
395
+ end
396
+
397
+ rule(:foreign_key_def_begin) do
398
+ (case_insensitive_str("constraint foreign key") >> something) |
399
+ (case_insensitive_str("constraint") >> something >> case_insensitive_str("foreign key")) |
400
+ (case_insensitive_str("foreign key")) |
401
+ (case_insensitive_str("foreign key") >> something)
402
+ end
403
+
404
+ rule(:parens_field_list) do
405
+ parenthetical(comma_separated(match('[^)]').repeat(1)))
406
+ end
407
+
408
+ # TODO: match_type.maybe >> on_delete.maybe >> on_update.maybe
409
+ rule(:reference_definition) do
410
+ case_insensitive_str("references") >> non(delimiter) >> parens_field_list.maybe
411
+ end
412
+
413
+ rule(:delimiter_statement) do
414
+ case_insensitive_str("delimiter") >> non(delimiter)
415
+ end
416
+
417
+ rule(:empty_statement) do
418
+ delimiter
419
+ end
420
+ end
421
+
422
+ class ParsletTransform < Parslet::Transform
423
+ # @example
424
+ # [
425
+ # { ... },
426
+ # { ... }
427
+ # ]
428
+ rule(statements: subtree(:statements)) do
429
+ case statements
430
+ when Array
431
+ statements
432
+ else
433
+ []
434
+ end
435
+ end
436
+
437
+ # @example
438
+ # "id"
439
+ rule(quoted_identifier: simple(:quoted_identifier)) do
440
+ quoted_identifier.to_s.gsub(/\A`(.+)`\z/, '\1')
441
+ end
442
+
443
+ # @example
444
+ # "utf8"
445
+ rule(identifier: simple(:identifier)) do
446
+ identifier.to_s
447
+ end
448
+
449
+ rule(field_value: simple(:field_value)) do
450
+ field_value.to_s
451
+ end
452
+
453
+ rule(
454
+ field_type_name: simple(:field_type_name),
455
+ field_name: simple(:field_name),
456
+ field_qualifiers: subtree(:field_qualifiers),
457
+ field_values: subtree(:field_values),
458
+ ) do
459
+ {
460
+ name: field_name.to_s,
461
+ type: field_type_name.to_s.downcase,
462
+ qualifiers: Array.wrap(field_qualifiers).map do |qualifier|
463
+ { qualifier: qualifier }
464
+ end,
465
+ values: Array.wrap(field_values),
466
+ }
467
+ end
468
+
469
+ rule(table_name: simple(:table_name), table_components: subtree(:table_components)) do
470
+ components = Array.wrap(table_components)
471
+
472
+ fields = components.select do |component|
473
+ component[:field]
474
+ end.compact
475
+
476
+ indices = components.select do |component|
477
+ component[:index]
478
+ end.compact
479
+
480
+ {
481
+ name: table_name,
482
+ fields: fields,
483
+ indices: indices,
484
+ }
485
+ end
486
+
487
+ rule(auto_increment_qualifier: simple(:auto_increment_qualifier)) do
488
+ {
489
+ type: "auto_increment",
490
+ }
491
+ end
492
+
493
+ rule(not_null_qualifier: simple(:not_null_qualifier)) do
494
+ {
495
+ type: "not_null",
496
+ }
497
+ end
498
+
499
+ rule(null_qualifier: simple(:null_qualifier)) do
500
+ {
501
+ type: "null",
502
+ }
503
+ end
504
+
505
+ rule(primary_key_qualifier: simple(:primary_key_qualifier)) do
506
+ {
507
+ type: "primary_key",
508
+ }
509
+ end
510
+
511
+ rule(character_set_qualifier: simple(:character_set_qualifier)) do
512
+ {
513
+ type: "character_set",
514
+ value: character_set_qualifier,
515
+ }
516
+ end
517
+
518
+ rule(collate_qualifier: simple(:collate_qualifier)) do
519
+ {
520
+ type: "collate",
521
+ value: collate_qualifier,
522
+ }
523
+ end
524
+
525
+ rule(unique_key_qualifier: simple(:unique_key_qualifier)) do
526
+ {
527
+ type: "unique_key",
528
+ }
529
+ end
530
+
531
+ rule(key_qualifier: simple(:key_qualifier)) do
532
+ {
533
+ type: "key",
534
+ }
535
+ end
536
+
537
+ rule(database_name: simple(:database_name)) do
538
+ {
539
+ database_name: database_name.to_s,
540
+ }
541
+ end
542
+
543
+ rule(primary_key: subtree(:primary_key)) do
544
+ primary_key.merge(primary: true)
545
+ end
546
+
547
+ rule(fulltext_index: subtree(:fulltext_index)) do
548
+ fulltext_index.merge(type: "fulltext")
549
+ end
550
+
551
+ rule(spatial_index: subtree(:spatial_index)) do
552
+ spatial_index.merge(type: "spatial")
553
+ end
554
+
555
+ rule(column_name: simple(:column_name)) do
556
+ {
557
+ column: column_name,
558
+ name: nil,
559
+ type: nil,
560
+ }
561
+ end
562
+
563
+ rule(
564
+ column_name: simple(:column_name),
565
+ index_type: simple(:index_type),
566
+ ) do
567
+ {
568
+ column: column_name,
569
+ name: nil,
570
+ type: index_type.try(:to_s).try(:downcase),
571
+ }
572
+ end
573
+
574
+ rule(
575
+ column_name: simple(:column_name),
576
+ index_name: simple(:index_name),
577
+ ) do
578
+ {
579
+ column: column_name,
580
+ name: index_name,
581
+ type: nil,
582
+ }
583
+ end
584
+
585
+ rule(
586
+ column_name: simple(:column_name),
587
+ index_name: simple(:index_name),
588
+ index_type: simple(:index_type),
589
+ ) do
590
+ {
591
+ column: column_name,
592
+ name: index_name,
593
+ type: index_type.try(:to_s).try(:downcase),
594
+ }
595
+ end
596
+ end
597
+ end
598
+ end
599
+ end
@@ -0,0 +1,75 @@
1
+ module Scheman
2
+ class Schema
3
+ def initialize(statements)
4
+ @statements = statements
5
+ end
6
+
7
+ def to_hash
8
+ @statements
9
+ end
10
+
11
+ # @return [Array<Hash>] An array of CREATE TABLE statements
12
+ def create_tables
13
+ @create_tables ||= @statements.select do |statement|
14
+ statement[:create_table]
15
+ end
16
+ end
17
+
18
+ # @return [Hash]
19
+ def tables_indexed_by_name
20
+ @tables_indexed_by_name ||= tables.index_by(&:name)
21
+ end
22
+
23
+ # TODO: We might want to calculate DROP TABLE and ALTER TABLE against to created tables
24
+ # @return [Array<Scheman::Schema::Table>] All tables to be created after applying this schema
25
+ def tables
26
+ @tables ||= create_tables.map do |create_table|
27
+ Table.new(create_table[:create_table])
28
+ end
29
+ end
30
+
31
+ # @return [Array<String>]
32
+ def table_names
33
+ tables.map(&:name)
34
+ end
35
+
36
+ class Table
37
+ def initialize(table)
38
+ @table = table
39
+ end
40
+
41
+ # @return [String]
42
+ def name
43
+ @table[:name]
44
+ end
45
+
46
+ # @return [Array]
47
+ def fields
48
+ @table[:fields].map do |field|
49
+ Field.new(field[:field])
50
+ end
51
+ end
52
+
53
+ # @return [Hash]
54
+ def fields_indexed_by_name
55
+ @fields_indexed_by_name ||= fields.index_by(&:name)
56
+ end
57
+ end
58
+
59
+ class Field
60
+ def initialize(field)
61
+ @field = field
62
+ end
63
+
64
+ # @return [String]
65
+ def name
66
+ @field[:name]
67
+ end
68
+
69
+ # @return [Hash]
70
+ def to_hash
71
+ @field
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,3 @@
1
+ module Scheman
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,6 @@
1
+ module Scheman
2
+ module Views
3
+ class Base
4
+ end
5
+ end
6
+ end