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