lilit-sql 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.
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: []