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,525 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # Turns an AST back into SQL. It works from the nodes' readers rather than from the tokens
5
+ # they were parsed from, so trees built by hand, or changed after parsing, print too. The
6
+ # output is canonical rather than a copy of the input: keywords come out in upper case,
7
+ # identifiers are quoted only when they have to be, and a compound operand gets parentheses
8
+ # unless it is the left operand of an operator at its own precedence, where SQLite's
9
+ # left-associativity already groups it.
10
+ class Printer
11
+ BARE_IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/
12
+
13
+ BINARY_OPERATORS = {
14
+ ALL: "AND", ANY: "OR", CONCAT: "||", EXTRACT: "->", RETRIEVE: "->>",
15
+ MULTIPLY: "*", DIVIDE: "/", MODULO: "%", ADD: "+", SUB: "-",
16
+ BIT_AND: "&", BIT_OR: "|", BIT_LSHIFT: "<<", BIT_RSHIFT: ">>",
17
+ BELOW: "<", ABOVE: ">", ATMOST: "<=", ATLEAST: ">=", EQUALS: "=", NOT_EQUALS: "!=",
18
+ IS: "IS", IS_NOT: "IS NOT", GLOB: "GLOB", NOT_GLOB: "NOT GLOB", REGEXP: "REGEXP",
19
+ NOT_REGEXP: "NOT REGEXP", MATCH: "MATCH", NOT_MATCH: "NOT MATCH", IN: "IN", NOT_IN: "NOT IN",
20
+ }.freeze
21
+
22
+ # Names that would read back as the start of an expression
23
+ EXPRESSION_KEYWORDS = (Parser::LITERAL_KEYWORDS + [:CAST, :RAISE]).freeze
24
+
25
+ # Levels Plume's parser and SQLite agree on: SQLite also puts = and IS with IN, LIKE and the rest
26
+ PRECEDENCE = {
27
+ ANY: 1, ALL: 2,
28
+ IN: 3, NOT_IN: 3, MATCH: 3, NOT_MATCH: 3, GLOB: 3, NOT_GLOB: 3, REGEXP: 3, NOT_REGEXP: 3,
29
+ EQUALS: 4, NOT_EQUALS: 4, IS: 4, IS_NOT: 4,
30
+ BELOW: 5, ABOVE: 5, ATMOST: 5, ATLEAST: 5,
31
+ BIT_AND: 6, BIT_OR: 6, BIT_LSHIFT: 6, BIT_RSHIFT: 6,
32
+ ADD: 7, SUB: 7,
33
+ MULTIPLY: 8, DIVIDE: 8, MODULO: 8,
34
+ CONCAT: 9, EXTRACT: 9, RETRIEVE: 9,
35
+ }.freeze
36
+
37
+ COMPOUND_OPERATORS = { UNION: "UNION", UNION_ALL: "UNION ALL", INTERSECT: "INTERSECT", EXCEPT: "EXCEPT" }.freeze
38
+ JOIN_KINDS = { COMMA: ",", INNER: "JOIN", LEFT: "LEFT JOIN", RIGHT: "RIGHT JOIN", FULL: "FULL JOIN", CROSS: "CROSS JOIN" }.freeze
39
+ FOREIGN_KEY_ACTIONS = { CASCADE: "CASCADE", RESTRICT: "RESTRICT", SET_NULL: "SET NULL", SET_DEFAULT: "SET DEFAULT", NO_ACTION: "NO ACTION" }.freeze
40
+
41
+ # Operands of these need parentheses to keep their grouping when printed inside another expression
42
+ COMPOUND_EXPRESSIONS = [BinaryExpression, UnaryExpression, TernaryExpression, LikeExpression, CollationExpression].freeze
43
+
44
+ Error = Class.new(StandardError)
45
+
46
+ def self.print(node)
47
+ new.print(node)
48
+ rescue *STACK_EXHAUSTED
49
+ # As with parsing, how deep is too deep depends on the stack the caller has left
50
+ raise Error, "statement nested too deeply to print"
51
+ end
52
+
53
+ def print(node)
54
+ case node
55
+ when nil then nil
56
+ when Array then node.map { |n| print(n) }.join(", ")
57
+ when Node then __send__(:"print_#{underscore(node.class.name.split("::").last)}", node)
58
+ # Expressions built by hand may hold plain values, which stand for the literal they spell
59
+ when LiteralValue then print(LiteralExpression.new(value: node))
60
+ else raise ArgumentError, "cannot print #{node.inspect}"
61
+ end
62
+ end
63
+
64
+ def identifier(name)
65
+ name = name.to_s
66
+ return name if name.ascii_only? && name.match?(BARE_IDENTIFIER) && bare_keyword_ok?(name.upcase.to_sym)
67
+
68
+ %("#{doubled(name, '"')}")
69
+ end
70
+
71
+ # Keywords SQLite falls back to treating as identifiers, like ROWID or KEY, need no quotes -
72
+ # except the ones that would read back as the start of an expression
73
+ def bare_keyword_ok?(word) = !Lexer::KEYWORDS.include?(word) || (Parser::TOKEN_FALLBACKS.include?(word) && !EXPRESSION_KEYWORDS.include?(word))
74
+
75
+ def string(value) = "'#{doubled(value.to_s, "'")}'"
76
+
77
+ private
78
+
79
+ # Done on the bytes, since TruffleRuby's gsub refuses a string that is not valid UTF-8
80
+ def doubled(text, quote) = text.b.gsub(quote, quote * 2).force_encoding(text.encoding)
81
+
82
+ def underscore(name) = name.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
83
+
84
+ def join(*parts) = parts.flatten.reject { |p| !p || (p.respond_to?(:empty?) && p.empty?) }.join(" ")
85
+
86
+ def qualified(*names) = names.compact.map { |n| identifier(n) }.join(".")
87
+
88
+ def operand(node) = (COMPOUND_EXPRESSIONS.any? { |klass| klass === node }) ? "(#{print(node)})" : print(node)
89
+
90
+ def alias_of(node) = node.alias && "AS #{identifier(node.alias)}"
91
+
92
+ def list(items) = Array(items).map { |item| print(item) }.join(", ")
93
+
94
+ def parenthesized_names(names) = names && "(#{names.map { |n| identifier(n) }.join(", ")})"
95
+
96
+ # -- statements
97
+
98
+ def print_explain_statement(node) = join(node.query_plan ? "EXPLAIN QUERY PLAN" : "EXPLAIN", print(node.statement))
99
+
100
+ def print_select_statement(node)
101
+ join(print(node.with_clause), print(node.body), order_and_limit(node))
102
+ end
103
+
104
+ def order_and_limit(node)
105
+ [
106
+ node.orderings && "ORDER BY #{list(node.orderings)}",
107
+ # `LIMIT offset, limit` keeps its order: swapping the two would renumber their `?` parameters
108
+ if node.offset && node.offset_tk_tok == :COMMA
109
+ "LIMIT #{print(node.offset)}, #{print(node.limit)}"
110
+ else
111
+ [node.limit && "LIMIT #{print(node.limit)}", node.offset && "OFFSET #{print(node.offset)}"]
112
+ end,
113
+ ]
114
+ end
115
+
116
+ def print_with_clause(node) = join("WITH", node.recursive && "RECURSIVE", list(node.ctes))
117
+
118
+ def print_common_table_expression(node)
119
+ materialized = { true => "MATERIALIZED", false => "NOT MATERIALIZED" }[node.materialized]
120
+ join(identifier(node.name) + (node.columns ? "(#{list(node.columns)})" : ""), "AS", materialized, "(#{print(node.select)})")
121
+ end
122
+
123
+ def print_compound_select(node)
124
+ join(print(node.left), COMPOUND_OPERATORS.fetch(node.operator), print(node.right))
125
+ end
126
+
127
+ def print_select_core(node)
128
+ join(
129
+ "SELECT", node.distinct && "DISTINCT", list(node.columns),
130
+ node.source && "FROM #{print(node.source)}",
131
+ node.condition && "WHERE #{print(node.condition)}",
132
+ node.groupings && "GROUP BY #{list(node.groupings)}",
133
+ node.having && "HAVING #{print(node.having)}",
134
+ node.windows && "WINDOW #{list(node.windows)}",
135
+ )
136
+ end
137
+
138
+ def print_values_clause(node) = "VALUES #{list(node.rows)}"
139
+ def print_values_row(node) = "(#{list(node.values)})"
140
+
141
+ def print_result_column(node) = join(print(node.result), alias_of(node))
142
+ def print_star_result_column(node) = node.table ? "#{identifier(node.table)}.*" : "*"
143
+
144
+ def print_window_definition(node) = "#{identifier(node.name)} AS (#{window_body(node)})"
145
+
146
+ def window_body(node)
147
+ join(
148
+ node.base_window_name && identifier(node.base_window_name),
149
+ node.partitions && "PARTITION BY #{list(node.partitions)}",
150
+ node.orderings && "ORDER BY #{list(node.orderings)}",
151
+ print(node.frame),
152
+ )
153
+ end
154
+
155
+ def print_insert_statement(node)
156
+ verb = node.or_action ? ((node.insert_kw_tok == :REPLACE) ? "REPLACE" : "INSERT OR #{node.or_action}") : "INSERT"
157
+ join(
158
+ print(node.with_clause), verb, "INTO", print(node.table),
159
+ node.columns && "(#{list(node.columns)})",
160
+ node.default_values ? "DEFAULT VALUES" : print(node.select),
161
+ node.upserts&.map { |u| print(u) },
162
+ print(node.returning),
163
+ )
164
+ end
165
+
166
+ def print_upsert_clause(node)
167
+ target = node.targets && "(#{list(node.targets)})"
168
+ action =
169
+ if node.action == :NOTHING
170
+ "DO NOTHING"
171
+ else
172
+ join("DO UPDATE SET", list(node.assignments), node.condition && "WHERE #{print(node.condition)}")
173
+ end
174
+ join("ON CONFLICT", target, node.target_condition && "WHERE #{print(node.target_condition)}", action)
175
+ end
176
+
177
+ def print_update_statement(node)
178
+ join(
179
+ print(node.with_clause), node.or_action ? "UPDATE OR #{node.or_action}" : "UPDATE", print(node.table),
180
+ "SET", list(node.assignments),
181
+ node.source && "FROM #{print(node.source)}",
182
+ node.condition && "WHERE #{print(node.condition)}",
183
+ print(node.returning), order_and_limit(node),
184
+ )
185
+ end
186
+
187
+ def print_delete_statement(node)
188
+ join(
189
+ print(node.with_clause), "DELETE FROM", print(node.table),
190
+ node.condition && "WHERE #{print(node.condition)}",
191
+ print(node.returning), order_and_limit(node),
192
+ )
193
+ end
194
+
195
+ def print_assignment(node)
196
+ target = (node.columns.size == 1 && !node.columns_lp?) ? print(node.columns[0]) : "(#{list(node.columns)})"
197
+ "#{target} = #{print(node.value)}"
198
+ end
199
+
200
+ def print_returning_clause(node) = "RETURNING #{list(node.columns)}"
201
+
202
+ def print_create_table_statement(node)
203
+ head = join("CREATE", node.temporary && "TEMP", "TABLE", node.if_not_exists && "IF NOT EXISTS", print(node.table))
204
+ return join(head, "AS", print(node.select_statement)) if node.select_statement
205
+
206
+ definitions = list([*node.columns, *node.constraints])
207
+ join(head, "(#{definitions})", node.options && list(node.options))
208
+ end
209
+
210
+ def print_create_view_statement(node)
211
+ join(
212
+ "CREATE", node.temporary && "TEMP", "VIEW", node.if_not_exists && "IF NOT EXISTS", print(node.view),
213
+ node.columns && "(#{list(node.columns)})", "AS", print(node.select),
214
+ )
215
+ end
216
+
217
+ def print_create_index_statement(node)
218
+ join(
219
+ "CREATE", node.unique && "UNIQUE", "INDEX", node.if_not_exists && "IF NOT EXISTS", print(node.index),
220
+ "ON", identifier(node.table), "(#{list(node.columns)})",
221
+ node.condition && "WHERE #{print(node.condition)}",
222
+ )
223
+ end
224
+
225
+ def print_create_virtual_table_statement(node)
226
+ arguments = node.arguments_lp? ? "(#{node.arguments.join(", ")})" : nil
227
+ join("CREATE VIRTUAL TABLE", node.if_not_exists && "IF NOT EXISTS", print(node.table), "USING", identifier(node.module_name) + arguments.to_s)
228
+ end
229
+
230
+ def print_create_trigger_statement(node)
231
+ event = node.columns ? "UPDATE OF #{list(node.columns)}" : node.event.to_s
232
+ join(
233
+ "CREATE", node.temporary && "TEMP", "TRIGGER", node.if_not_exists && "IF NOT EXISTS", print(node.trigger),
234
+ node.time&.to_s&.tr("_", " "), event, "ON", print(node.table),
235
+ node.for_each_row_kw? && "FOR EACH ROW", node.condition && "WHEN #{print(node.condition)}",
236
+ "BEGIN", node.statements.map { |s| "#{print(s)};" }, "END",
237
+ )
238
+ end
239
+
240
+ def print_alter_table_statement(node)
241
+ action =
242
+ case node.action
243
+ when :RENAME_TABLE then "RENAME TO #{identifier(node.new_name)}"
244
+ when :RENAME_COLUMN then "RENAME COLUMN #{identifier(node.column)} TO #{identifier(node.new_name)}"
245
+ when :ADD_COLUMN then "ADD COLUMN #{print(node.column_definition)}"
246
+ when :DROP_COLUMN then "DROP COLUMN #{identifier(node.column)}"
247
+ when :ADD_CONSTRAINT then "ADD #{print(node.constraint)}"
248
+ when :DROP_CONSTRAINT then "DROP CONSTRAINT #{identifier(node.constraint_name)}"
249
+ when :SET_NOT_NULL then join("ALTER COLUMN #{identifier(node.column)} SET NOT NULL", print(node.conflict_clause))
250
+ when :DROP_NOT_NULL then "ALTER COLUMN #{identifier(node.column)} DROP NOT NULL"
251
+ end
252
+ join("ALTER TABLE", print(node.table), action)
253
+ end
254
+
255
+ def print_drop_statement(node) = join("DROP", node.type.to_s, node.if_exists && "IF EXISTS", print(node.target))
256
+
257
+ def print_begin_statement(node) = join("BEGIN", node.type&.to_s)
258
+ def print_commit_statement(_node) = "COMMIT"
259
+ def print_rollback_statement(node) = join("ROLLBACK", node.savepoint && "TO SAVEPOINT #{identifier(node.savepoint)}")
260
+ def print_savepoint_statement(node) = "SAVEPOINT #{identifier(node.name)}"
261
+ def print_release_statement(node) = "RELEASE SAVEPOINT #{identifier(node.name)}"
262
+
263
+ def print_pragma_statement(node)
264
+ name = qualified(node.schema, node.name)
265
+ return "PRAGMA #{name}" if node.value.nil?
266
+
267
+ value =
268
+ case node.value
269
+ when Numeric then node.value.to_s
270
+ else (node.value_tk_tok == :STRING) ? string(node.value) : identifier(node.value)
271
+ end
272
+ (node.eq_tk_tok == :LP) ? "PRAGMA #{name}(#{value})" : "PRAGMA #{name} = #{value}"
273
+ end
274
+
275
+ def print_attach_statement(node)
276
+ join("ATTACH DATABASE", print(node.file), "AS", print(node.schema), node.key && "KEY #{print(node.key)}")
277
+ end
278
+
279
+ def print_detach_statement(node) = "DETACH DATABASE #{print(node.schema)}"
280
+ def print_analyze_statement(node) = join("ANALYZE", print(node.target))
281
+ def print_reindex_statement(node) = join("REINDEX", print(node.target))
282
+ def print_vacuum_statement(node) = join("VACUUM", node.schema && identifier(node.schema), node.into && "INTO #{print(node.into)}")
283
+
284
+ # -- FROM
285
+
286
+ def print_table_name(node)
287
+ indexed = node.indexed_by ? "INDEXED BY #{identifier(node.indexed_by)}" : (node.not_indexed && "NOT INDEXED")
288
+ join(qualified(node.schema, node.table), alias_of(node), indexed)
289
+ end
290
+
291
+ def print_table_function(node)
292
+ join("#{qualified(node.schema, node.function)}(#{list(node.arguments)})", alias_of(node))
293
+ end
294
+
295
+ def print_subquery_source(node) = join("(#{print(node.select)})", alias_of(node))
296
+ def print_parenthetical_source(node) = join("(#{print(node.source)})", alias_of(node))
297
+
298
+ def print_join_clause(node)
299
+ node.joins.reduce(print(node.left)) do |sql, each|
300
+ operator = print(each.operator)
301
+ right = join(print(each.source), print(each.constraint))
302
+ (operator == ",") ? "#{sql}, #{right}" : "#{sql} #{operator} #{right}"
303
+ end
304
+ end
305
+
306
+ def print_join_operator(node) = join(node.natural && "NATURAL", JOIN_KINDS.fetch(node.kind))
307
+ def print_join_on_constraint(node) = "ON #{print(node.expression)}"
308
+ def print_join_using_constraint(node) = "USING (#{list(node.columns)})"
309
+
310
+ # -- expressions
311
+
312
+ def print_column_name(node) = qualified(node.schema, node.table, node.column)
313
+
314
+ def print_literal_expression(node)
315
+ # Numbers keep their spelling, so 0x1F stays hex and 1e3 stays an exponent
316
+ return node.value_tk_src if node.value_tk? && node.value_tk_spn.last == :numeric
317
+
318
+ case (value = node.value)
319
+ when nil then "NULL"
320
+ when true then "TRUE"
321
+ when false then "FALSE"
322
+ when Symbol then value.to_s
323
+ when Integer then value.to_s
324
+ when Float
325
+ # SQLite has no literal for NaN or infinity
326
+ value.finite? ? value.to_s : raise(ArgumentError, "cannot print #{value}")
327
+ when String
328
+ (value.encoding == Encoding::BINARY) ? "X'#{value.unpack1("H*").upcase}'" : string(value)
329
+ # A Rational would print as integer division (1/3), a Complex as nothing SQL can read
330
+ else raise ArgumentError, "cannot print #{value.inspect}"
331
+ end
332
+ end
333
+
334
+ def print_variable(node) = node.name
335
+
336
+ # Every binary operator associates to the left, so a left operand at the same precedence needs no
337
+ # parentheses: `a - b + c`, `a OR b OR c`. A long chain then prints flat rather than nesting a pair
338
+ # per term, and the left spine is walked in a loop, so its length costs no stack
339
+ def print_binary_expression(node)
340
+ chain = [node]
341
+ while BinaryExpression === (left = chain.last.left) && PRECEDENCE[left.operator] == PRECEDENCE[node.operator]
342
+ chain << left
343
+ end
344
+
345
+ chain.reverse_each.reduce(+operand(chain.last.left)) do |sql, link|
346
+ sql << " " << BINARY_OPERATORS.fetch(link.operator) << " " << operand(link.right)
347
+ end
348
+ end
349
+
350
+ def print_unary_expression(node)
351
+ case node.operator
352
+ when :NOT then "NOT #{operand(node.operand)}"
353
+ when :NEGATE then "-#{unary_operand(node.operand)}"
354
+ when :IDENTITY then "+#{unary_operand(node.operand)}"
355
+ when :INVERT then "~#{unary_operand(node.operand)}"
356
+ when :IS_NULL then "#{operand(node.operand)} ISNULL"
357
+ when :NOT_NULL then "#{operand(node.operand)} NOTNULL"
358
+ end
359
+ end
360
+
361
+ # Two minus signs in a row would start a comment
362
+ def unary_operand(node)
363
+ sql = operand(node)
364
+ sql.start_with?("-", "+") ? "(#{sql})" : sql
365
+ end
366
+
367
+ def print_ternary_expression(node)
368
+ operator = (node.operator == :NOT_BETWEEN) ? "NOT BETWEEN" : "BETWEEN"
369
+ "#{operand(node.left)} #{operator} #{operand(node.middle)} AND #{operand(node.right)}"
370
+ end
371
+
372
+ def print_like_expression(node)
373
+ operator = (node.operator == :NOT_LIKE) ? "NOT LIKE" : "LIKE"
374
+ join("#{operand(node.left)} #{operator} #{operand(node.right)}", node.escape && "ESCAPE #{operand(node.escape)}")
375
+ end
376
+ alias_method :print_not_like_expression, :print_like_expression
377
+
378
+ def print_collation_expression(node) = "#{operand(node.expression)} COLLATE #{identifier(node.sequence)}"
379
+
380
+ def print_parenthetical_expression(node) = "(#{print(node.contents)})"
381
+
382
+ def print_cast_expression(node) = "CAST(#{join(print(node.expression), "AS", print(node.as))})"
383
+
384
+ def print_case_expression(node)
385
+ join("CASE", node.predicate && print(node.predicate), list_of(node.conditions), node.else_clause && "ELSE #{print(node.else_clause)}", "END")
386
+ end
387
+
388
+ def list_of(items) = items.map { |item| print(item) }
389
+
390
+ def print_case_condition(node) = "WHEN #{print(node.predicate)} THEN #{print(node.consequence)}"
391
+
392
+ def print_function_reference(node)
393
+ join(
394
+ "#{qualified(node.schema, node.function)}(#{print(node.arguments)})",
395
+ node.within_group && "WITHIN GROUP (ORDER BY #{print(node.within_group)})",
396
+ node.filter_clause && "FILTER (WHERE #{print(node.filter_clause)})",
397
+ print(node.over_clause),
398
+ )
399
+ end
400
+
401
+ def print_function_arguments(node)
402
+ quantifier = node.distinct ? "DISTINCT" : (node.distinct_kw_tok == :ALL && "ALL")
403
+ join(quantifier, list(node.expressions), node.order_by && "ORDER BY #{list(node.order_by)}")
404
+ end
405
+
406
+ def print_star_function_argument(_node) = "*"
407
+ def print_empty_function_argument(_node) = ""
408
+
409
+ def print_over_clause(node)
410
+ return "OVER #{identifier(node.window_name)}" if node.window_name
411
+
412
+ "OVER (#{window_body(node)})"
413
+ end
414
+
415
+ def print_frame_spec(node)
416
+ type = node.type.to_s.upcase
417
+ exclude = node.exclude && "EXCLUDE #{node.exclude.to_s.upcase.tr("_", " ")}"
418
+ if node.ending_boundary
419
+ join(type, "BETWEEN", print(node.starting_boundary), "AND", print(node.ending_boundary), exclude)
420
+ else
421
+ join(type, print(node.starting_boundary), exclude)
422
+ end
423
+ end
424
+
425
+ def print_frame_boundary(node)
426
+ type = node.type.to_s.upcase.tr("_", " ")
427
+ return type if type == "CURRENT ROW"
428
+
429
+ value = node.value
430
+ join((Symbol === value || String === value) ? value.to_s.upcase : print(value), type)
431
+ end
432
+
433
+ def print_subquery_expression(node) = "(#{print(node.select)})"
434
+ def print_exists_expression(node) = "EXISTS (#{print(node.select)})"
435
+
436
+ def print_raise_expression(node)
437
+ return "RAISE(IGNORE)" if node.type == :IGNORE
438
+
439
+ "RAISE(#{node.type}, #{print(node.error_message)})"
440
+ end
441
+
442
+ def print_ordering_term(node)
443
+ join(
444
+ print(node.expression), node.collation && "COLLATE #{identifier(node.collation)}",
445
+ node.direction&.to_s&.upcase, node.nulls && "NULLS #{node.nulls.to_s.upcase}",
446
+ )
447
+ end
448
+
449
+ # -- column and table definitions
450
+
451
+ def print_column_definition(node) = join(identifier(node.name), print(node.type), list_of(Array(node.constraints)))
452
+ def print_column_type(node) = node.text
453
+
454
+ def constraint_name(node) = node.name && "CONSTRAINT #{identifier(node.name)}"
455
+
456
+ def print_primary_key_column_constraint(node)
457
+ join(constraint_name(node), "PRIMARY KEY", node.direction&.to_s, print(node.conflict_clause), node.autoincrement && "AUTOINCREMENT")
458
+ end
459
+
460
+ def print_not_null_column_constraint(node) = join(constraint_name(node), "NOT NULL", print(node.conflict_clause))
461
+ def print_null_column_constraint(node) = join(constraint_name(node), "NULL", print(node.conflict_clause))
462
+ def print_unique_column_constraint(node) = join(constraint_name(node), "UNIQUE", print(node.conflict_clause))
463
+ def print_check_column_constraint(node) = join(constraint_name(node), "CHECK (#{print(node.expression)})", print(node.conflict_clause))
464
+ def print_collate_column_constraint(node) = join(constraint_name(node), "COLLATE #{identifier(node.collation)}")
465
+ def print_foreign_key_column_constraint(node) = join(constraint_name(node), print(node.foreign_key_clause))
466
+ def print_no_op_column_constraint(node) = constraint_name(node)
467
+
468
+ def print_default_column_constraint(node)
469
+ return join(constraint_name(node), "DEFAULT #{node.value_tk_src}") if node.value_tk?
470
+
471
+ value = node.value
472
+ sql =
473
+ case value
474
+ when Node then "(#{print(value)})"
475
+ else print(LiteralExpression.new(value:))
476
+ end
477
+ join(constraint_name(node), "DEFAULT #{sql}")
478
+ end
479
+
480
+ def print_generated_as_column_constraint(node)
481
+ join(constraint_name(node), "GENERATED ALWAYS AS (#{print(node.expression)})", node.type&.to_s)
482
+ end
483
+
484
+ def print_conflict_clause(node) = "ON CONFLICT #{node.resolution}"
485
+
486
+ def print_primary_key_table_constraint(node)
487
+ join(constraint_name(node), "PRIMARY KEY (#{list(node.columns)}#{" AUTOINCREMENT" if node.autoincrement})", print(node.conflict_clause))
488
+ end
489
+
490
+ def print_unique_table_constraint(node) = join(constraint_name(node), "UNIQUE (#{list(node.columns)})", print(node.conflict_clause))
491
+ def print_check_table_constraint(node) = join(constraint_name(node), "CHECK (#{print(node.expression)})", print(node.conflict_clause))
492
+ def print_no_op_table_constraint(node) = constraint_name(node)
493
+
494
+ def print_deferrable_column_constraint(node) = join(constraint_name(node), deferrable(node) || "DEFERRABLE")
495
+
496
+ # The words as written, or else what `deferred` says
497
+ def deferrable(node)
498
+ return Array(node.deferrable_span_tok).join(" ") if node.deferrable_span?
499
+
500
+ "DEFERRABLE INITIALLY DEFERRED" if node.deferred
501
+ end
502
+
503
+ def print_foreign_key_table_constraint(node)
504
+ join(constraint_name(node), "FOREIGN KEY (#{list(node.columns)})", print(node.foreign_key_clause))
505
+ end
506
+
507
+ def print_foreign_key_clause(node)
508
+ join(
509
+ "REFERENCES", identifier(node.table), node.columns && "(#{list(node.columns)})",
510
+ list_of(Array(node.actions)), list_of(Array(node.match_clauses)),
511
+ deferrable(node),
512
+ )
513
+ end
514
+
515
+ def print_foreign_key_action(node) = "ON #{node.trigger.to_s.upcase} #{FOREIGN_KEY_ACTIONS.fetch(node.action)}"
516
+ def print_match_clause(node) = "MATCH #{node.type}"
517
+
518
+ def print_indexed_column(node)
519
+ join(identifier(node.name), node.collation && "COLLATE #{identifier(node.collation)}", node.direction&.to_s, node.nulls && "NULLS #{node.nulls.to_s.upcase}")
520
+ end
521
+
522
+ def print_strict_table_option(_node) = "STRICT"
523
+ def print_without_rowid_table_option(_node) = "WITHOUT ROWID"
524
+ end
525
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+
6
+ module Plume
7
+ module SQLiteGrammar
8
+ extend self
9
+
10
+ PARSE_Y_URI = URI("https://raw.githubusercontent.com/sqlite/sqlite/refs/heads/master/src/parse.y").freeze
11
+ RULE_PATTERN = /^(?:.+::=[^.]+\.)|(?:%token_class[^.]+\.)/
12
+ IGNORED_SNIPPETS = [
13
+ %r{^//},
14
+ /^scanpt.+::=/,
15
+ /^scantok.+::=/,
16
+ ].freeze
17
+ TOKEN_CLASS_SIGIL = "%token_class "
18
+
19
+ def rules
20
+ return @hash if defined? @hash
21
+
22
+ # extract the SQL statements from the test files
23
+ @hash = Hash.new { |h, k| h[k] = [] }
24
+ matches = contents.scan(RULE_PATTERN)
25
+ matches.each do |match|
26
+ result = match.tap(&:strip!)
27
+ next if IGNORED_SNIPPETS.any? { |it| result.match?(it) }
28
+
29
+ if result.start_with?(TOKEN_CLASS_SIGIL)
30
+ rule, definition = result.gsub(TOKEN_CLASS_SIGIL, "").split(/\s+/)
31
+ else
32
+ rule, definition = result.gsub(/\(.*?\)/, "").split(/\s+::=\s+/)
33
+ end
34
+
35
+ tokens = definition.chomp(".").tap(&:strip!).split(/\s+/)
36
+ next (@hash[rule.strip] << tokens) if tokens.empty?
37
+
38
+ sequences = tokens.map { |it| it.split("|") }.reduce(&:product).map { |it| Array(it).flatten }
39
+ sequences.each { |it| @hash[rule.strip] << it.reject { |it| ["scanpt", "scantok"].include?(it) } }
40
+ end
41
+
42
+ @hash
43
+ end
44
+
45
+ def token_names
46
+ rules
47
+ .values
48
+ .flatten
49
+ .uniq
50
+ .select { |it| it.getbyte(0) <= 90 }
51
+ end
52
+
53
+ def rule_names
54
+ rules.keys
55
+ end
56
+
57
+ def rules_with(token_or_rule)
58
+ rules.select do |_, sequences|
59
+ sequences.any? { |sequence| sequence.include? token_or_rule }
60
+ end
61
+ end
62
+
63
+ def contents
64
+ return @contents if defined? @contents
65
+
66
+ # download the sqlite `parse.y` file for the master branch
67
+ Net::HTTP.start(PARSE_Y_URI.host, PARSE_Y_URI.port, use_ssl: true) do |http|
68
+ request = Net::HTTP::Get.new(PARSE_Y_URI)
69
+ response = http.request(request)
70
+
71
+ # Handle redirects
72
+ if response.is_a?(Net::HTTPRedirection)
73
+ uri = URI(response["location"])
74
+ response = Net::HTTP.get_response(uri)
75
+ end
76
+
77
+ if response.is_a?(Net::HTTPSuccess)
78
+ @contents = response.body
79
+ return @contents
80
+ else
81
+ raise "Error: #{response.code} - #{response.message}"
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # SQL shortened to fit a length, for logs and error trackers, by putting `...` where the parts
5
+ # least needed to recognise the query were: result columns, WHERE conditions, VALUES rows, CTE
6
+ # bodies, INSERT column lists and UPDATE assignments - the most deeply nested first, the longest
7
+ # first among those. Everything else stays as written. When that is not enough, the end is cut off
8
+ module Truncate
9
+ extend self
10
+
11
+ ELLIPSIS = "..."
12
+
13
+ def call(sql, max_length)
14
+ return sql if sql.length <= max_length
15
+
16
+ src = sql.b
17
+ chosen = []
18
+ result = sql
19
+ candidates(Parser.new(sql).parse).each do |beg, fin, _depth|
20
+ next if fin - beg <= ELLIPSIS.bytesize || chosen.any? { |b, f| b <= beg && fin <= f }
21
+
22
+ chosen.reject! { |b, f| beg <= b && f <= fin }
23
+ chosen << [beg, fin]
24
+ result = render(src, chosen, sql.encoding)
25
+ return result if result.length <= max_length
26
+ end
27
+ "#{result[0, [max_length - ELLIPSIS.length, 0].max]}#{ELLIPSIS}"[0, max_length]
28
+ end
29
+
30
+ # Byte ranges that may be cut, with how deeply they are nested, in the order to cut them
31
+ def candidates(statements)
32
+ found = []
33
+ stack = statements.reverse.map { |s| [s, 0] }
34
+ while (node, depth = stack.pop)
35
+ span =
36
+ case node
37
+ when SelectCore then [list_span(node.columns), node.condition&.location]
38
+ when ValuesClause then [list_span(node.rows)]
39
+ when CommonTableExpression then [node.select&.location]
40
+ when InsertStatement then [list_span(node.columns)]
41
+ when UpdateStatement then [list_span(node.assignments)]
42
+ else []
43
+ end
44
+ span.compact.each { |beg, fin| found << [beg, fin, depth] }
45
+ node.children.reverse_each { |child| stack << [child, depth + 1] }
46
+ end
47
+ found.sort_by { |beg, fin, depth| [-depth, beg - fin] }
48
+ end
49
+
50
+ private
51
+
52
+ def list_span(nodes)
53
+ first, last = nodes&.first&.location, nodes&.last&.location
54
+ [first[0], last[1]] if first && last
55
+ end
56
+
57
+ def render(src, spans, encoding)
58
+ out = +""
59
+ at = 0
60
+ spans.sort.each do |beg, fin|
61
+ out << src.byteslice(at, beg - at) << ELLIPSIS
62
+ at = fin
63
+ end
64
+ (out << src.byteslice(at, src.bytesize - at)).force_encoding(encoding)
65
+ end
66
+ end
67
+ end