lilit-sql 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/lilit_sql.rb +630 -0
  3. metadata +48 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8055fa9bdd9f4c7dd21d9e1036ca22ebd423637994ec0aeda7894062a8c3b2fa
4
+ data.tar.gz: 2683c306cb68f4d0e7b2e17fd678e8951a67c92a28acdafc87ffbf0c2b466ba0
5
+ SHA512:
6
+ metadata.gz: 67a42477621655368a38a0ef15c55af70046b859b69f7bddd780546debd7bb8b81267112896a358effb95ae0b5820ec73618bd8f074412b0f10ebda78ff177b4
7
+ data.tar.gz: bab58672731d51626c65ee9855aa7fc7ba88c11bec2d917dbdfb6349923dc60424bbc9fee0e591e4b556224959a28315a0b259b68e956a5a51cab6899095b019
data/lib/lilit_sql.rb ADDED
@@ -0,0 +1,630 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby2ruby'
4
+ require 'ruby_parser'
5
+ require 'sourcify'
6
+
7
+ class From
8
+ attr_accessor :source
9
+ attr_accessor :join_type
10
+ attr_accessor :condition
11
+
12
+ def initialize(source, join_type = nil, condition = nil, alias_name = nil)
13
+ @source = source
14
+ @join_type = join_type
15
+ @condition = condition
16
+ @alias_name = alias_name
17
+ end
18
+
19
+ def alias_name=(value)
20
+ @alias_name = value
21
+ end
22
+
23
+ def raw_alias_name
24
+ @alias_name
25
+ end
26
+
27
+ def alias_name
28
+ @alias_name || @source.subquery_name
29
+ end
30
+
31
+ def rows
32
+ source.rows
33
+ end
34
+ end
35
+
36
+ class GroupBy
37
+ attr_accessor :query
38
+ attr_accessor :keys
39
+
40
+ def initialize(query, keys)
41
+ @query = query
42
+ @keys = keys
43
+ end
44
+
45
+ def aggregate(&blk)
46
+ result = blk.call(@keys, *@query.rows)
47
+
48
+ Query.new(
49
+ @query.froms + [],
50
+ @query.conditions + [],
51
+ @keys,
52
+ Row.new(result.class.members, result)
53
+ )
54
+ end
55
+ end
56
+
57
+ class Row
58
+ attr_accessor :columns
59
+
60
+ def initialize(columns, origins = [])
61
+ @columns = columns.zip(origins).map do |col, origin|
62
+ if col.is_a?(Symbol)
63
+ Column.new(col, origin)
64
+ elsif col.is_a?(Column)
65
+ col
66
+ else
67
+ raise NotImplementedError
68
+ end
69
+ end
70
+ end
71
+
72
+ def col(name)
73
+ found = @columns.select {|c| c.name == name}.first
74
+
75
+ raise ArgumentError.new("#{name} is not found in the columns: #{@columns.map {|c|c.name}.inspect}") if found.nil?
76
+
77
+ found
78
+ end
79
+
80
+ def with_from(from)
81
+ Row.new(@columns.map {|c| c.with_from(from)})
82
+ end
83
+
84
+ private def method_missing(symbol, *args)
85
+ begin
86
+ col(symbol)
87
+ rescue ArgumentError
88
+ super
89
+ end
90
+ end
91
+
92
+ def has?(name)
93
+ @columns.any? {|c| c.name == name}
94
+ end
95
+
96
+ def decl_sql
97
+ @columns.map {|c|c.decl_sql}.join(', ')
98
+ end
99
+ end
100
+
101
+ class Column
102
+ attr_accessor :name
103
+ attr_accessor :origin
104
+
105
+ def initialize(name, origin = nil, from = nil)
106
+ @name = name
107
+ @origin = origin
108
+ @from = from
109
+ end
110
+
111
+ def with_from(from)
112
+ Column.new(@name, @origin, from)
113
+ end
114
+
115
+ def eq(other)
116
+ Expr.new(self, :eq, other)
117
+ end
118
+
119
+ def in(list)
120
+ Expr.new(self, :in, list)
121
+ end
122
+
123
+ def *(other)
124
+ Expr.new(self, :*, other)
125
+ end
126
+
127
+ def <=(other)
128
+ Expr.new(self, :<=, other)
129
+ end
130
+
131
+ def ref_sql
132
+ "#{@from.alias_name}.#{@name}"
133
+ end
134
+
135
+ def decl_sql
136
+ s = ''
137
+ if origin
138
+ origin_sql = if origin.is_a?(Proc)
139
+ origin.call.ref_sql
140
+ else
141
+ origin.ref_sql
142
+ end
143
+ if origin_sql != @name.to_s
144
+ s += "#{origin_sql} as "
145
+ end
146
+ end
147
+ s += @name.to_s
148
+ s
149
+ end
150
+
151
+ def ==(other)
152
+ other.class == self.class && other.state == self.state
153
+ end
154
+
155
+ def state
156
+ self.instance_variables.map { |variable| self.instance_variable_get variable }
157
+ end
158
+ end
159
+
160
+ class Count < Column
161
+
162
+ def initialize
163
+ super(nil)
164
+ end
165
+
166
+ def ref_sql
167
+ decl_sql
168
+ end
169
+
170
+ def decl_sql
171
+ "count(*)"
172
+ end
173
+ end
174
+
175
+ class Sum < Column
176
+
177
+ def initialize(col)
178
+ super(nil, col)
179
+ end
180
+
181
+ def ref_sql
182
+ decl_sql
183
+ end
184
+
185
+ def decl_sql
186
+ "sum(#{@origin.ref_sql})"
187
+ end
188
+ end
189
+
190
+ module Aggregate
191
+ def self.count
192
+ Count.new
193
+ end
194
+
195
+ def self.sum(col)
196
+ Sum.new(col)
197
+ end
198
+ end
199
+
200
+ class Literal
201
+ attr_accessor :value
202
+
203
+ def initialize(value)
204
+ @value = value
205
+ end
206
+
207
+ def ref_sql
208
+ decl_sql
209
+ end
210
+
211
+ def decl_sql
212
+ if @value.nil?
213
+ "null"
214
+ elsif @value.is_a?(Integer) || @value.is_a?(Float)
215
+ "#{@value}"
216
+ elsif @value.is_a?(String)
217
+ "'#{@value}'"
218
+ else
219
+ raise NotImplementedError.new("Literal doesn't support render #{@value.class} (#{@value})")
220
+ end
221
+ end
222
+
223
+ def ==(other)
224
+ other.class == self.class && other.state == self.state
225
+ end
226
+
227
+ def state
228
+ self.instance_variables.map { |variable| self.instance_variable_get variable }
229
+ end
230
+ end
231
+
232
+ class Expr
233
+ attr_accessor :left
234
+ attr_accessor :op
235
+ attr_accessor :right
236
+
237
+ def initialize(left, op, right)
238
+ @left = left
239
+ @op = op
240
+ @right = right
241
+ end
242
+
243
+ def and(other)
244
+ Expr.new(self, :and, other)
245
+ end
246
+
247
+ def ref_sql
248
+ if op == :and
249
+ "#{left.ref_sql} and #{right.ref_sql}"
250
+ elsif op == :eq
251
+ if right.is_a?(Literal) && right.value.nil?
252
+ "#{left.ref_sql} is #{right.ref_sql}"
253
+ else
254
+ "#{left.ref_sql} = #{right.ref_sql}"
255
+ end
256
+ elsif op == :ne
257
+ if right.is_a?(Literal) && right.value.nil?
258
+ "#{left.ref_sql} is not #{right.ref_sql}"
259
+ else
260
+ "#{left.ref_sql} != #{right.ref_sql}"
261
+ end
262
+ elsif op == :*
263
+ "#{left.ref_sql} * #{right.ref_sql}"
264
+ elsif op == :<=
265
+ "#{left.ref_sql} <= #{right.ref_sql}"
266
+ elsif op == :in
267
+ "#{left.ref_sql} in (#{right.map {|r|r.ref_sql}.join(', ')})"
268
+ else
269
+ raise ArgumentError.new("#{op} is not supported by Expr")
270
+ end
271
+ end
272
+
273
+ def ==(other)
274
+ other.class == self.class && other.state == self.state
275
+ end
276
+
277
+ def state
278
+ self.instance_variables.map { |variable| self.instance_variable_get variable }
279
+ end
280
+ end
281
+
282
+ class Table
283
+ attr_accessor :table_name
284
+ attr_accessor :rows
285
+
286
+ def initialize(struct, table_name)
287
+ @table_name = table_name
288
+ @rows = [Row.new(struct.members)]
289
+ end
290
+
291
+ def subquery_name
292
+ @table_name.to_s
293
+ end
294
+ end
295
+
296
+ class Query
297
+ attr_accessor :froms
298
+ attr_accessor :conditions
299
+ attr_accessor :grouped_keys
300
+ attr_accessor :row
301
+
302
+ def initialize(froms, conditions = [], grouped_keys = [], row = nil)
303
+ @froms = froms + []
304
+ @conditions = conditions + []
305
+ @grouped_keys = grouped_keys + []
306
+ @row = row
307
+ @subquery_name = nil
308
+ end
309
+
310
+ def self.from(query)
311
+ new([From.new(query)])
312
+ end
313
+
314
+ def is_vanilla
315
+ @froms.size == 1 && @conditions.empty? && @grouped_keys.empty? && @row.nil?
316
+ end
317
+
318
+ def map(&blk)
319
+ if @row
320
+ return Query.from(self).map(&blk)
321
+ end
322
+
323
+ result = expr(&blk).call(*get_from_rows)
324
+ Query.new(
325
+ @froms,
326
+ @conditions,
327
+ @grouped_keys,
328
+ Row.new(result.class.members, result)
329
+ )
330
+ end
331
+
332
+ def has?(column_name)
333
+ rows.any? {|r| r.has?(column_name)}
334
+ end
335
+
336
+ def rows
337
+ if @row
338
+ return [@row]
339
+ end
340
+
341
+ get_from_rows
342
+ end
343
+
344
+ def group_by(&blk)
345
+ if @row
346
+ return Query.from(self).group_by(&blk)
347
+ end
348
+
349
+ result = expr(&blk).call(*get_from_rows)
350
+
351
+ if result.is_a?(Column)
352
+ GroupBy.new(self, [result])
353
+ elsif result.is_a?(Array)
354
+ GroupBy.new(self, result)
355
+ else
356
+ raise NotImplementedError
357
+ end
358
+ end
359
+
360
+ def join(other, &blk)
361
+ perform_join(:join, other, &blk)
362
+ end
363
+
364
+ def left_join(other, &blk)
365
+ perform_join(:left_join, other, &blk)
366
+ end
367
+
368
+ def where(&blk)
369
+ if @row
370
+ return Query.from(self).where(&blk)
371
+ end
372
+
373
+ condition = expr(&blk).call(*get_from_rows)
374
+ Query.new(
375
+ @froms,
376
+ @conditions + [condition],
377
+ @grouped_keys,
378
+ @row
379
+ )
380
+ end
381
+
382
+ def subquery_name
383
+ if is_vanilla
384
+ return @froms.first.source.subquery_name
385
+ end
386
+
387
+ if @subquery_name.nil?
388
+ raise ArgumentError.new("The query #{self.inspect} doesn't have a subquery name")
389
+ end
390
+
391
+ @subquery_name
392
+ end
393
+
394
+ def subquery_name=(value)
395
+ @subquery_name = value
396
+ end
397
+
398
+ def sql
399
+ s = "select "
400
+ s += rows.map {|r| r.decl_sql}.join(', ')
401
+ s += " from"
402
+
403
+ @froms.each_with_index do |from, index|
404
+ if index >= 1
405
+ if from.join_type == :join
406
+ s += " join"
407
+ elsif from.join_type == :left_join
408
+ s += " left join"
409
+ else
410
+ raise ArgumentError.new("The join type #{from.join_type} is not supoprted.")
411
+ end
412
+ end
413
+
414
+ s += " #{from.source.subquery_name}"
415
+
416
+ if from.source.subquery_name != from.alias_name
417
+ s += " #{from.alias_name}"
418
+ end
419
+
420
+ if from.condition
421
+ s += " on #{from.condition.ref_sql}"
422
+ end
423
+ end
424
+
425
+ if @conditions.size > 0
426
+ s += " where #{@conditions.map {|c| c.ref_sql}.join(' and ')}"
427
+ end
428
+
429
+ if @grouped_keys.size > 0
430
+ s += " group by #{@grouped_keys.map {|k| k.ref_sql}.join(', ')}"
431
+ end
432
+
433
+ s
434
+ end
435
+
436
+ private
437
+ def get_from_rows
438
+ @froms.map {|f| f.rows.map {|r|r.with_from(f)}}.flatten
439
+ end
440
+
441
+ def get_next_alias
442
+ alias_names = @froms.map {|f|f.raw_alias_name}.compact
443
+ index = 0
444
+ alias_names.sort.each do |name|
445
+ if name == "alias#{index}"
446
+ index += 1
447
+ end
448
+ end
449
+ "alias#{index}"
450
+ end
451
+
452
+ def perform_join(join_type, other, &blk)
453
+ if @row || @conditions.size > 0
454
+ return Query.from(self).send(:perform_join, join_type, other, &blk)
455
+ end
456
+
457
+ alias_name = nil
458
+ @froms.each do |from|
459
+ if from.source == other
460
+ alias_name = get_next_alias
461
+ break
462
+ end
463
+ end
464
+
465
+ other_from = From.new(other, join_type, nil, alias_name)
466
+ condition = expr(&blk).call(*(get_from_rows + other_from.rows.map {|r|r.with_from(other_from)}))
467
+ other_from.condition = condition
468
+
469
+ Query.new(
470
+ @froms + [other_from],
471
+ @conditions,
472
+ @grouped_keys,
473
+ @row
474
+ )
475
+ end
476
+ end
477
+
478
+ class IfElse
479
+ def initialize(cond, true_result, false_result)
480
+ @condition = cond
481
+ @true_result = true_result
482
+ @false_result = false_result
483
+ end
484
+
485
+ def ref_sql
486
+ "if(#{@condition.ref_sql}, #{@true_result.ref_sql}, #{@false_result.ref_sql})"
487
+ end
488
+
489
+ def decl_sql
490
+ ref_sql
491
+ end
492
+
493
+ def ==(other)
494
+ other.class == self.class && other.state == self.state
495
+ end
496
+
497
+ def state
498
+ self.instance_variables.map { |variable| self.instance_variable_get variable }
499
+ end
500
+ end
501
+
502
+ def ifElse(cond, true_result, false_result)
503
+ IfElse.new(cond, true_result, false_result)
504
+ end
505
+
506
+ def generate_sql(query)
507
+ queries = fill(query)
508
+ last_query = queries.pop
509
+
510
+ sql = ''
511
+
512
+ if queries.size > 0
513
+ sql += 'with '
514
+ end
515
+
516
+ queries.map.with_index do |query, index|
517
+ if index > 0
518
+ sql += ', '
519
+ end
520
+ query.subquery_name = "subquery#{index}"
521
+ sql += "#{query.subquery_name} as (\n#{query.sql}\n)\n"
522
+ end
523
+
524
+ sql += last_query.sql
525
+
526
+ sql
527
+ end
528
+
529
+ def fill(query)
530
+ return [] if query.is_vanilla
531
+
532
+ queries = []
533
+ query.froms.each do |from|
534
+ if from.source.is_a?(Query)
535
+ subqueries = fill(from.source)
536
+ subqueries.each do |subquery|
537
+ queries.push(subquery)
538
+ end
539
+ end
540
+ end
541
+ queries.push(query)
542
+ queries.uniq
543
+ end
544
+
545
+ $ruby2ruby = Ruby2Ruby.new
546
+
547
+ def search_for_expr_block(parsed)
548
+ # s(:iter, s(:call, nil, :expr)
549
+
550
+ if parsed[0] == :iter && parsed[1][0] == :call && parsed[1][1].nil? && parsed[1][2] == :expr
551
+ return parsed[3]
552
+ end
553
+
554
+ parsed.each do |component|
555
+ if component.is_a?(Sexp)
556
+ return search_for_expr_block(component)
557
+ end
558
+ end
559
+
560
+ nil
561
+ end
562
+
563
+ def rewrite(parsed)
564
+ parsed = parsed.map do |component|
565
+ if component.is_a?(Sexp)
566
+ rewrite(component)
567
+ else
568
+ component
569
+ end
570
+ end
571
+
572
+ if parsed[0] == :call && parsed[2] == :==
573
+ parsed[2] = :eq
574
+ elsif parsed[0] == :and
575
+ parsed = Sexp.new(
576
+ :call,
577
+ parsed[1],
578
+ :and,
579
+ parsed[2]
580
+ )
581
+ elsif parsed[0] == :str
582
+ parsed = Sexp.new(
583
+ :call,
584
+ Sexp.new(:const, :Literal),
585
+ :new,
586
+ Sexp.new(:str, parsed[1])
587
+ )
588
+ elsif parsed[0] == :lit && (parsed[1].is_a?(Integer) || parsed[1].is_a?(Float))
589
+ parsed = Sexp.new(
590
+ :call,
591
+ Sexp.new(:const, :Literal),
592
+ :new,
593
+ Sexp.new(:lit, parsed[1])
594
+ )
595
+ elsif parsed[0] == :case && parsed[2] && parsed[2][0] == :in
596
+ parsed = Sexp.new(
597
+ :call,
598
+ parsed[1],
599
+ :in,
600
+ parsed[2][1]
601
+ )
602
+ elsif parsed[0] == :if
603
+ parsed = Sexp.new(
604
+ :call,
605
+ nil,
606
+ :ifElse,
607
+ parsed[1],
608
+ parsed[2],
609
+ parsed[3],
610
+ )
611
+ elsif parsed[0] == :nil
612
+ parsed = Sexp.new(
613
+ :call,
614
+ Sexp.new(:const, :Literal),
615
+ :new,
616
+ Sexp.new(:nil)
617
+ )
618
+ end
619
+
620
+ parsed
621
+ end
622
+
623
+ def expr(&blk)
624
+ parsed = blk.to_sexp
625
+
626
+ parsed = rewrite(parsed)
627
+
628
+ code = $ruby2ruby.process(parsed)
629
+ eval(code, blk.binding)
630
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lilit-sql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Tanin Na Nakorn
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-06-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |-
14
+ lilit-sql is a Ruby DSL for composing production-grade analytical SQLs
15
+
16
+ The DSL supports higher order primitives like parameterization and meta-programming, which makes writing production-grade analytical SQLs easier.
17
+
18
+ This is suitable for an application that builds analytics on top of SQL-supported data warehouses like Presto.
19
+ email: "@tanin"
20
+ executables: []
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - lib/lilit_sql.rb
25
+ homepage: https://github.com/tanin47/lilit-sql
26
+ licenses:
27
+ - MIT
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubygems_version: 3.4.10
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: lilit-sql is a Ruby DSL for composing production-grade analytical SQLs
48
+ test_files: []