plume 0.1.0 → 0.3.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.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +506 -11
  3. data/lib/plume/analysis/resolver.rb +797 -0
  4. data/lib/plume/analysis.rb +211 -0
  5. data/lib/plume/ast/alter_table_statement.rb +40 -0
  6. data/lib/plume/ast/analyze_statement.rb +15 -0
  7. data/lib/plume/ast/assignment.rb +18 -0
  8. data/lib/plume/ast/attach_statement.rb +17 -0
  9. data/lib/plume/ast/begin_statement.rb +17 -0
  10. data/lib/plume/ast/binary_expression.rb +71 -0
  11. data/lib/plume/ast/case_condition.rb +29 -0
  12. data/lib/plume/ast/case_expression.rb +22 -0
  13. data/lib/plume/ast/cast_expression.rb +16 -0
  14. data/lib/plume/ast/check_column_constraint.rb +20 -0
  15. data/lib/plume/ast/check_table_constraint.rb +20 -0
  16. data/lib/plume/ast/collate_column_constraint.rb +19 -0
  17. data/lib/plume/ast/collation_expression.rb +26 -0
  18. data/lib/plume/ast/column_constraint.rb +7 -0
  19. data/lib/plume/ast/column_definition.rb +16 -0
  20. data/lib/plume/ast/column_name.rb +22 -0
  21. data/lib/plume/ast/column_type.rb +26 -0
  22. data/lib/plume/ast/commit_statement.rb +13 -0
  23. data/lib/plume/ast/common_table_expression.rb +31 -0
  24. data/lib/plume/ast/compound_select.rb +30 -0
  25. data/lib/plume/ast/conflict_clause.rb +22 -0
  26. data/lib/plume/ast/conflict_resolution.rb +20 -0
  27. data/lib/plume/ast/create_index_statement.rb +29 -0
  28. data/lib/plume/ast/create_table_statement.rb +47 -0
  29. data/lib/plume/ast/create_trigger_statement.rb +43 -0
  30. data/lib/plume/ast/create_view_statement.rb +30 -0
  31. data/lib/plume/ast/create_virtual_table_statement.rb +28 -0
  32. data/lib/plume/ast/default_column_constraint.rb +29 -0
  33. data/lib/plume/ast/deferrable_column_constraint.rb +20 -0
  34. data/lib/plume/ast/delete_statement.rb +28 -0
  35. data/lib/plume/ast/detach_statement.rb +13 -0
  36. data/lib/plume/ast/drop_statement.rb +21 -0
  37. data/lib/plume/ast/empty_function_argument.rb +6 -0
  38. data/lib/plume/ast/exists_expression.rb +15 -0
  39. data/lib/plume/ast/explain_statement.rb +15 -0
  40. data/lib/plume/ast/expression.rb +30 -0
  41. data/lib/plume/ast/foreign_key_action.rb +26 -0
  42. data/lib/plume/ast/foreign_key_clause.rb +50 -0
  43. data/lib/plume/ast/foreign_key_column_constraint.rb +16 -0
  44. data/lib/plume/ast/foreign_key_table_constraint.rb +20 -0
  45. data/lib/plume/ast/frame_boundary.rb +35 -0
  46. data/lib/plume/ast/frame_spec.rb +34 -0
  47. data/lib/plume/ast/function_arguments.rb +18 -0
  48. data/lib/plume/ast/function_reference.rb +29 -0
  49. data/lib/plume/ast/generated_as_column_constraint.rb +24 -0
  50. data/lib/plume/ast/indexed_column.rb +24 -0
  51. data/lib/plume/ast/insert_statement.rb +35 -0
  52. data/lib/plume/ast/join.rb +12 -0
  53. data/lib/plume/ast/join_clause.rb +15 -0
  54. data/lib/plume/ast/join_on_constraint.rb +12 -0
  55. data/lib/plume/ast/join_operator.rb +31 -0
  56. data/lib/plume/ast/join_using_constraint.rb +16 -0
  57. data/lib/plume/ast/like_expression.rb +19 -0
  58. data/lib/plume/ast/literal_expression.rb +29 -0
  59. data/lib/plume/ast/literal_false.rb +7 -0
  60. data/lib/plume/ast/literal_nil.rb +7 -0
  61. data/lib/plume/ast/literal_value.rb +18 -0
  62. data/lib/plume/ast/match_clause.rb +15 -0
  63. data/lib/plume/ast/node.rb +330 -0
  64. data/lib/plume/ast/noop_column_constraint.rb +15 -0
  65. data/lib/plume/ast/noop_table_constraint.rb +15 -0
  66. data/lib/plume/ast/not_like_expression.rb +7 -0
  67. data/lib/plume/ast/not_null_column_constraint.rb +17 -0
  68. data/lib/plume/ast/null_column_constraint.rb +17 -0
  69. data/lib/plume/ast/ordering_term.rb +32 -0
  70. data/lib/plume/ast/over_clause.rb +25 -0
  71. data/lib/plume/ast/parenthetical_expression.rb +37 -0
  72. data/lib/plume/ast/parenthetical_source.rb +19 -0
  73. data/lib/plume/ast/pragma_statement.rb +26 -0
  74. data/lib/plume/ast/primary_key_column_constraint.rb +23 -0
  75. data/lib/plume/ast/primary_key_table_constraint.rb +23 -0
  76. data/lib/plume/ast/raise_expression.rb +23 -0
  77. data/lib/plume/ast/reindex_statement.rb +15 -0
  78. data/lib/plume/ast/release_statement.rb +16 -0
  79. data/lib/plume/ast/result_column.rb +18 -0
  80. data/lib/plume/ast/returning_clause.rb +12 -0
  81. data/lib/plume/ast/rollback_statement.rb +19 -0
  82. data/lib/plume/ast/savepoint_statement.rb +16 -0
  83. data/lib/plume/ast/select_core.rb +27 -0
  84. data/lib/plume/ast/select_statement.rb +34 -0
  85. data/lib/plume/ast/star_function_argument.rb +6 -0
  86. data/lib/plume/ast/star_result_column.rb +16 -0
  87. data/lib/plume/ast/strict_table_option.rb +6 -0
  88. data/lib/plume/ast/subquery_expression.rb +14 -0
  89. data/lib/plume/ast/subquery_source.rb +19 -0
  90. data/lib/plume/ast/table_constraint.rb +7 -0
  91. data/lib/plume/ast/table_function.rb +28 -0
  92. data/lib/plume/ast/table_name.rb +28 -0
  93. data/lib/plume/ast/table_option.rb +7 -0
  94. data/lib/plume/ast/ternary_expression.rb +26 -0
  95. data/lib/plume/ast/token.rb +183 -0
  96. data/lib/plume/ast/unary_expression.rb +36 -0
  97. data/lib/plume/ast/unique_column_constraint.rb +17 -0
  98. data/lib/plume/ast/unique_table_constraint.rb +20 -0
  99. data/lib/plume/ast/update_statement.rb +35 -0
  100. data/lib/plume/ast/upsert_clause.rb +26 -0
  101. data/lib/plume/ast/vacuum_statement.rb +18 -0
  102. data/lib/plume/ast/values_clause.rb +12 -0
  103. data/lib/plume/ast/values_row.rb +13 -0
  104. data/lib/plume/ast/variable.rb +15 -0
  105. data/lib/plume/ast/window_definition.rb +26 -0
  106. data/lib/plume/ast/with_clause.rb +15 -0
  107. data/lib/plume/ast/without_rowid_table_option.rb +6 -0
  108. data/lib/plume/catalog.rb +276 -0
  109. data/lib/plume/comment.rb +73 -0
  110. data/lib/plume/dump.rb +143 -0
  111. data/lib/plume/fingerprint.rb +86 -0
  112. data/lib/plume/lexer.rb +640 -0
  113. data/lib/plume/literal_patches.rb +76 -0
  114. data/lib/plume/normalize.rb +65 -0
  115. data/lib/plume/parameters.rb +52 -0
  116. data/lib/plume/parser.rb +4064 -0
  117. data/lib/plume/pretty_printer.rb +202 -0
  118. data/lib/plume/printer.rb +525 -0
  119. data/lib/plume/sqlite_grammar.rb +86 -0
  120. data/lib/plume/truncate.rb +67 -0
  121. data/lib/plume/union.rb +28 -0
  122. data/lib/plume/version.rb +1 -1
  123. data/lib/plume.rb +274 -3
  124. metadata +176 -12
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # The printer laid out over lines: each clause starts a line, a clause too long for
5
+ # `max_line_length` puts its items one per line, indented by `indent_size`, and a long AND or OR
6
+ # chain in a condition puts each operand on a line of its own. Subqueries are indented inside
7
+ # their parentheses. Expressions otherwise print as `Printer` prints them, so what comes out
8
+ # parses into the same tree
9
+ class PrettyPrinter < Printer
10
+ def self.print(node, indent_size: 4, max_line_length: 80, commas_start_of_line: false)
11
+ new(indent_size:, max_line_length:, commas_start_of_line:).print(node)
12
+ rescue *STACK_EXHAUSTED
13
+ raise Error, "statement nested too deeply to print"
14
+ end
15
+
16
+ def initialize(indent_size:, max_line_length:, commas_start_of_line:)
17
+ super()
18
+ @pad = " " * indent_size
19
+ @max = max_line_length
20
+ @leading_commas = commas_start_of_line
21
+ end
22
+
23
+ private
24
+
25
+ def lines(*parts) = parts.flatten.reject { |p| !p || p.empty? }.join("\n")
26
+
27
+ # Only the line breaks between tokens: one inside a string or quoted name is part of its value
28
+ def indent(text)
29
+ src = text.b
30
+ breaks = []
31
+ lexer = Lexer.new(text)
32
+ while (token = lexer.next_token)
33
+ next unless token == :SPACE
34
+
35
+ ((lexer.anchor)...(lexer.cursor)).each { |i| breaks << i if src.getbyte(i) == 10 && i + 1 < src.bytesize && src.getbyte(i + 1) != 10 }
36
+ end
37
+
38
+ out = (src.empty? ? +"" : @pad.b.dup)
39
+ at = 0
40
+ breaks.each do |i|
41
+ out << src.byteslice(at, i + 1 - at) << @pad
42
+ at = i + 1
43
+ end
44
+ (out << src.byteslice(at, src.bytesize - at)).force_encoding(text.encoding)
45
+ end
46
+
47
+ # Lines after the first of a multi-line operand line up under the clause it sits in
48
+ def hanging(text) = text.include?("\n") ? indent(text).delete_prefix(@pad) : text
49
+
50
+ def fits?(text) = !text.include?("\n") && text.length <= @max
51
+
52
+ # `KEYWORD a, b, c` on one line if it fits, otherwise the keyword alone and an item per line
53
+ def clause(keyword, items)
54
+ items = Array(items).map { |item| (String === item) ? item : print(item) }
55
+ one_line = "#{keyword} #{items.join(", ")}"
56
+ return one_line if fits?(one_line)
57
+
58
+ body =
59
+ if @leading_commas
60
+ items.each_with_index.map { |item, i| i.zero? ? " #{item}" : ", #{item}" }.join("\n")
61
+ else
62
+ items.join(",\n")
63
+ end
64
+ "#{keyword}\n#{indent(body)}"
65
+ end
66
+
67
+ # A condition on one line if it fits, otherwise its top-level AND or OR chain an operand per line
68
+ def condition(keyword, node)
69
+ one_line = "#{keyword} #{print(node)}"
70
+ return one_line if fits?(one_line) || !(BinaryExpression === node && (node.operator in :ALL | :ANY))
71
+
72
+ chain = [node]
73
+ chain << chain.last.left while BinaryExpression === chain.last.left && chain.last.left.operator == node.operator
74
+ word = BINARY_OPERATORS.fetch(node.operator)
75
+ rest = chain.reverse_each.map { |link| "#{word} #{operand(link.right)}" }
76
+ lines("#{keyword} #{hanging(operand(chain.last.left))}", rest.map { |line| indent(line) })
77
+ end
78
+
79
+ def parens(inner) = inner.include?("\n") ? "(\n#{indent(inner)}\n)" : "(#{inner})"
80
+
81
+ def pretty_order_and_limit(node)
82
+ limit =
83
+ if node.offset && node.offset_tk_tok == :COMMA
84
+ "LIMIT #{print(node.offset)}, #{print(node.limit)}"
85
+ else
86
+ join(node.limit && "LIMIT #{print(node.limit)}", node.offset && "OFFSET #{print(node.offset)}")
87
+ end
88
+ [node.orderings && clause("ORDER BY", node.orderings), limit]
89
+ end
90
+
91
+ # -- statements
92
+
93
+ def print_select_statement(node) = lines(print(node.with_clause), print(node.body), pretty_order_and_limit(node))
94
+
95
+ def print_with_clause(node)
96
+ ctes = node.ctes.map { |cte| print(cte) }
97
+ head = join("WITH", node.recursive && "RECURSIVE")
98
+ "#{head} #{ctes.join(",\n")}"
99
+ end
100
+
101
+ def print_common_table_expression(node)
102
+ materialized = { true => "MATERIALIZED", false => "NOT MATERIALIZED" }[node.materialized]
103
+ name = identifier(node.name) + (node.columns ? "(#{list(node.columns)})" : "")
104
+ join(name, "AS", materialized, parens(print(node.select)))
105
+ end
106
+
107
+ def print_compound_select(node) = lines(print(node.left), COMPOUND_OPERATORS.fetch(node.operator), print(node.right))
108
+
109
+ def print_select_core(node)
110
+ lines(
111
+ clause(node.distinct ? "SELECT DISTINCT" : "SELECT", node.columns),
112
+ node.source && from(node.source),
113
+ node.condition && condition("WHERE", node.condition),
114
+ node.groupings && clause("GROUP BY", node.groupings),
115
+ node.having && condition("HAVING", node.having),
116
+ node.windows && clause("WINDOW", node.windows),
117
+ )
118
+ end
119
+
120
+ def from(source) = (JoinClause === source) ? "FROM #{print(source)}" : clause("FROM", [source])
121
+
122
+ def print_values_clause(node) = clause("VALUES", node.rows)
123
+
124
+ def print_join_clause(node)
125
+ node.joins.reduce(print(node.left)) do |sql, each|
126
+ operator = print(each.operator)
127
+ right = join(print(each.source), print(each.constraint))
128
+ (operator == ",") ? "#{sql},\n#{right}" : "#{sql}\n#{operator} #{right}"
129
+ end
130
+ end
131
+
132
+ def print_subquery_source(node) = join(parens(print(node.select)), alias_of(node))
133
+ def print_subquery_expression(node) = parens(print(node.select))
134
+ def print_exists_expression(node) = "EXISTS #{parens(print(node.select))}"
135
+
136
+ def print_insert_statement(node)
137
+ verb = node.or_action ? ((node.insert_kw_tok == :REPLACE) ? "REPLACE" : "INSERT OR #{node.or_action}") : "INSERT"
138
+ lines(
139
+ print(node.with_clause),
140
+ join(verb, "INTO", print(node.table), node.columns && "(#{list(node.columns)})"),
141
+ node.default_values ? "DEFAULT VALUES" : print(node.select),
142
+ node.upserts&.map { |u| print(u) },
143
+ print(node.returning),
144
+ )
145
+ end
146
+
147
+ def print_upsert_clause(node)
148
+ target = node.targets && "(#{list(node.targets)})"
149
+ head = join("ON CONFLICT", target, node.target_condition && "WHERE #{print(node.target_condition)}")
150
+ return join(head, "DO NOTHING") if node.action == :NOTHING
151
+
152
+ lines(head, indent(lines(clause("DO UPDATE SET", node.assignments), node.condition && condition("WHERE", node.condition))))
153
+ end
154
+
155
+ def print_update_statement(node)
156
+ lines(
157
+ print(node.with_clause),
158
+ join(node.or_action ? "UPDATE OR #{node.or_action}" : "UPDATE", print(node.table)),
159
+ clause("SET", node.assignments),
160
+ node.source && from(node.source),
161
+ node.condition && condition("WHERE", node.condition),
162
+ print(node.returning), pretty_order_and_limit(node),
163
+ )
164
+ end
165
+
166
+ def print_delete_statement(node)
167
+ lines(
168
+ print(node.with_clause), "DELETE FROM #{print(node.table)}",
169
+ node.condition && condition("WHERE", node.condition),
170
+ print(node.returning), pretty_order_and_limit(node),
171
+ )
172
+ end
173
+
174
+ def print_returning_clause(node) = clause("RETURNING", node.columns)
175
+
176
+ def print_create_table_statement(node)
177
+ head = join("CREATE", node.temporary && "TEMP", "TABLE", node.if_not_exists && "IF NOT EXISTS", print(node.table))
178
+ return lines("#{head} AS", print(node.select_statement)) if node.select_statement
179
+
180
+ definitions = [*node.columns, *node.constraints].map { |d| print(d) }
181
+ join("#{head} (\n#{indent(definitions.join(",\n"))}\n)", node.options && list(node.options))
182
+ end
183
+
184
+ def print_create_view_statement(node)
185
+ head = join(
186
+ "CREATE", node.temporary && "TEMP", "VIEW", node.if_not_exists && "IF NOT EXISTS", print(node.view),
187
+ node.columns && "(#{list(node.columns)})", "AS",
188
+ )
189
+ lines(head, print(node.select))
190
+ end
191
+
192
+ def print_create_trigger_statement(node)
193
+ event = node.columns ? "UPDATE OF #{list(node.columns)}" : node.event.to_s
194
+ head = join(
195
+ "CREATE", node.temporary && "TEMP", "TRIGGER", node.if_not_exists && "IF NOT EXISTS", print(node.trigger),
196
+ node.time&.to_s&.tr("_", " "), event, "ON", print(node.table),
197
+ node.for_each_row_kw? && "FOR EACH ROW", node.condition && "WHEN #{print(node.condition)}",
198
+ )
199
+ lines(head, "BEGIN", node.statements.map { |s| indent("#{print(s)};") }, "END")
200
+ end
201
+ end
202
+ end