plume 0.1.0 → 0.2.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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +280 -10
  3. data/lib/plume/analysis/resolver.rb +574 -0
  4. data/lib/plume/analysis.rb +135 -0
  5. data/lib/plume/ast/alter_table_statement.rb +29 -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 +19 -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 +25 -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/delete_statement.rb +28 -0
  34. data/lib/plume/ast/detach_statement.rb +13 -0
  35. data/lib/plume/ast/drop_statement.rb +21 -0
  36. data/lib/plume/ast/empty_function_argument.rb +6 -0
  37. data/lib/plume/ast/exists_expression.rb +15 -0
  38. data/lib/plume/ast/explain_statement.rb +15 -0
  39. data/lib/plume/ast/expression.rb +30 -0
  40. data/lib/plume/ast/foreign_key_action.rb +26 -0
  41. data/lib/plume/ast/foreign_key_clause.rb +50 -0
  42. data/lib/plume/ast/foreign_key_column_constraint.rb +16 -0
  43. data/lib/plume/ast/foreign_key_table_constraint.rb +20 -0
  44. data/lib/plume/ast/frame_boundary.rb +35 -0
  45. data/lib/plume/ast/frame_spec.rb +34 -0
  46. data/lib/plume/ast/function_arguments.rb +17 -0
  47. data/lib/plume/ast/function_reference.rb +29 -0
  48. data/lib/plume/ast/generated_as_column_constraint.rb +24 -0
  49. data/lib/plume/ast/indexed_column.rb +21 -0
  50. data/lib/plume/ast/insert_statement.rb +35 -0
  51. data/lib/plume/ast/join.rb +12 -0
  52. data/lib/plume/ast/join_clause.rb +15 -0
  53. data/lib/plume/ast/join_on_constraint.rb +12 -0
  54. data/lib/plume/ast/join_operator.rb +31 -0
  55. data/lib/plume/ast/join_using_constraint.rb +16 -0
  56. data/lib/plume/ast/like_expression.rb +19 -0
  57. data/lib/plume/ast/literal_expression.rb +29 -0
  58. data/lib/plume/ast/literal_false.rb +7 -0
  59. data/lib/plume/ast/literal_nil.rb +7 -0
  60. data/lib/plume/ast/literal_value.rb +18 -0
  61. data/lib/plume/ast/match_clause.rb +15 -0
  62. data/lib/plume/ast/node.rb +207 -0
  63. data/lib/plume/ast/noop_column_constraint.rb +15 -0
  64. data/lib/plume/ast/noop_table_constraint.rb +15 -0
  65. data/lib/plume/ast/not_like_expression.rb +7 -0
  66. data/lib/plume/ast/not_null_column_constraint.rb +17 -0
  67. data/lib/plume/ast/null_column_constraint.rb +17 -0
  68. data/lib/plume/ast/ordering_term.rb +32 -0
  69. data/lib/plume/ast/over_clause.rb +25 -0
  70. data/lib/plume/ast/parenthetical_expression.rb +37 -0
  71. data/lib/plume/ast/parenthetical_source.rb +19 -0
  72. data/lib/plume/ast/pragma_statement.rb +26 -0
  73. data/lib/plume/ast/primary_key_column_constraint.rb +23 -0
  74. data/lib/plume/ast/primary_key_table_constraint.rb +23 -0
  75. data/lib/plume/ast/raise_expression.rb +23 -0
  76. data/lib/plume/ast/reindex_statement.rb +15 -0
  77. data/lib/plume/ast/release_statement.rb +16 -0
  78. data/lib/plume/ast/result_column.rb +18 -0
  79. data/lib/plume/ast/returning_clause.rb +12 -0
  80. data/lib/plume/ast/rollback_statement.rb +19 -0
  81. data/lib/plume/ast/savepoint_statement.rb +16 -0
  82. data/lib/plume/ast/select_core.rb +27 -0
  83. data/lib/plume/ast/select_statement.rb +34 -0
  84. data/lib/plume/ast/star_function_argument.rb +6 -0
  85. data/lib/plume/ast/star_result_column.rb +16 -0
  86. data/lib/plume/ast/strict_table_option.rb +6 -0
  87. data/lib/plume/ast/subquery_expression.rb +14 -0
  88. data/lib/plume/ast/subquery_source.rb +19 -0
  89. data/lib/plume/ast/table_constraint.rb +7 -0
  90. data/lib/plume/ast/table_function.rb +28 -0
  91. data/lib/plume/ast/table_name.rb +28 -0
  92. data/lib/plume/ast/table_option.rb +7 -0
  93. data/lib/plume/ast/ternary_expression.rb +26 -0
  94. data/lib/plume/ast/token.rb +180 -0
  95. data/lib/plume/ast/unary_expression.rb +36 -0
  96. data/lib/plume/ast/unique_column_constraint.rb +17 -0
  97. data/lib/plume/ast/unique_table_constraint.rb +20 -0
  98. data/lib/plume/ast/update_statement.rb +35 -0
  99. data/lib/plume/ast/upsert_clause.rb +26 -0
  100. data/lib/plume/ast/vacuum_statement.rb +18 -0
  101. data/lib/plume/ast/values_clause.rb +12 -0
  102. data/lib/plume/ast/values_row.rb +13 -0
  103. data/lib/plume/ast/variable.rb +15 -0
  104. data/lib/plume/ast/window_definition.rb +26 -0
  105. data/lib/plume/ast/with_clause.rb +15 -0
  106. data/lib/plume/ast/without_rowid_table_option.rb +6 -0
  107. data/lib/plume/catalog.rb +180 -0
  108. data/lib/plume/lexer.rb +639 -0
  109. data/lib/plume/parser.rb +4015 -0
  110. data/lib/plume/printer.rb +471 -0
  111. data/lib/plume/sqlite_grammar.rb +86 -0
  112. data/lib/plume/union.rb +28 -0
  113. data/lib/plume/version.rb +1 -1
  114. data/lib/plume.rb +143 -3
  115. metadata +167 -12
@@ -0,0 +1,471 @@
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 every compound operand gets
8
+ # parentheses so precedence never has to be second-guessed.
9
+ class Printer
10
+ BARE_IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/
11
+
12
+ BINARY_OPERATORS = {
13
+ ALL: "AND", ANY: "OR", CONCAT: "||", EXTRACT: "->", RETRIEVE: "->>",
14
+ MULTIPLY: "*", DIVIDE: "/", MODULO: "%", ADD: "+", SUB: "-",
15
+ BIT_AND: "&", BIT_OR: "|", BIT_LSHIFT: "<<", BIT_RSHIFT: ">>",
16
+ BELOW: "<", ABOVE: ">", ATMOST: "<=", ATLEAST: ">=", EQUALS: "=", NOT_EQUALS: "!=",
17
+ IS: "IS", IS_NOT: "IS NOT", GLOB: "GLOB", NOT_GLOB: "NOT GLOB", REGEXP: "REGEXP",
18
+ NOT_REGEXP: "NOT REGEXP", MATCH: "MATCH", NOT_MATCH: "NOT MATCH", IN: "IN", NOT_IN: "NOT IN",
19
+ }.freeze
20
+
21
+ COMPOUND_OPERATORS = { UNION: "UNION", UNION_ALL: "UNION ALL", INTERSECT: "INTERSECT", EXCEPT: "EXCEPT" }.freeze
22
+ JOIN_KINDS = { COMMA: ",", INNER: "JOIN", LEFT: "LEFT JOIN", RIGHT: "RIGHT JOIN", FULL: "FULL JOIN", CROSS: "CROSS JOIN" }.freeze
23
+ FOREIGN_KEY_ACTIONS = { CASCADE: "CASCADE", RESTRICT: "RESTRICT", SET_NULL: "SET NULL", SET_DEFAULT: "SET DEFAULT", NO_ACTION: "NO ACTION" }.freeze
24
+
25
+ # Operands of these need parentheses to keep their grouping when printed inside another expression
26
+ COMPOUND_EXPRESSIONS = [BinaryExpression, UnaryExpression, TernaryExpression, LikeExpression, CollationExpression].freeze
27
+
28
+ def self.print(node) = new.print(node)
29
+
30
+ def print(node)
31
+ case node
32
+ when nil then nil
33
+ when Array then node.map { |n| print(n) }.join(", ")
34
+ when Node then __send__(:"print_#{underscore(node.class.name.split("::").last)}", node)
35
+ # Expressions built by hand may hold plain values, which stand for the literal they spell
36
+ when LiteralValue then print(LiteralExpression.new(value: node))
37
+ else raise ArgumentError, "cannot print #{node.inspect}"
38
+ end
39
+ end
40
+
41
+ def identifier(name)
42
+ name = name.to_s
43
+ return name if name.match?(BARE_IDENTIFIER) && bare_keyword_ok?(name.upcase.to_sym)
44
+
45
+ %("#{name.gsub('"', '""')}")
46
+ end
47
+
48
+ # Keywords SQLite falls back to treating as identifiers, like ROWID or KEY, need no quotes
49
+ def bare_keyword_ok?(word) = !Lexer::KEYWORDS.include?(word) || Parser::TOKEN_FALLBACKS.include?(word)
50
+
51
+ def string(value) = "'#{value.to_s.gsub("'", "''")}'"
52
+
53
+ private
54
+
55
+ def underscore(name) = name.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
56
+
57
+ def join(*parts) = parts.flatten.reject { |p| !p || (p.respond_to?(:empty?) && p.empty?) }.join(" ")
58
+
59
+ def qualified(*names) = names.compact.map { |n| identifier(n) }.join(".")
60
+
61
+ def operand(node) = (COMPOUND_EXPRESSIONS.any? { |klass| klass === node }) ? "(#{print(node)})" : print(node)
62
+
63
+ def alias_of(node) = node.alias && "AS #{identifier(node.alias)}"
64
+
65
+ def list(items) = Array(items).map { |item| print(item) }.join(", ")
66
+
67
+ def parenthesized_names(names) = names && "(#{names.map { |n| identifier(n) }.join(", ")})"
68
+
69
+ # -- statements
70
+
71
+ def print_explain_statement(node) = join(node.query_plan ? "EXPLAIN QUERY PLAN" : "EXPLAIN", print(node.statement))
72
+
73
+ def print_select_statement(node)
74
+ join(print(node.with_clause), print(node.body), order_and_limit(node))
75
+ end
76
+
77
+ def order_and_limit(node)
78
+ [
79
+ node.orderings && "ORDER BY #{list(node.orderings)}",
80
+ node.limit && "LIMIT #{print(node.limit)}",
81
+ node.offset && "OFFSET #{print(node.offset)}",
82
+ ]
83
+ end
84
+
85
+ def print_with_clause(node) = join("WITH", node.recursive && "RECURSIVE", list(node.ctes))
86
+
87
+ def print_common_table_expression(node)
88
+ materialized = { true => "MATERIALIZED", false => "NOT MATERIALIZED" }[node.materialized]
89
+ join(identifier(node.name) + (node.columns ? "(#{list(node.columns)})" : ""), "AS", materialized, "(#{print(node.select)})")
90
+ end
91
+
92
+ def print_compound_select(node)
93
+ join(print(node.left), COMPOUND_OPERATORS.fetch(node.operator), print(node.right))
94
+ end
95
+
96
+ def print_select_core(node)
97
+ join(
98
+ "SELECT", node.distinct && "DISTINCT", list(node.columns),
99
+ node.source && "FROM #{print(node.source)}",
100
+ node.condition && "WHERE #{print(node.condition)}",
101
+ node.groupings && "GROUP BY #{list(node.groupings)}",
102
+ node.having && "HAVING #{print(node.having)}",
103
+ node.windows && "WINDOW #{list(node.windows)}",
104
+ )
105
+ end
106
+
107
+ def print_values_clause(node) = "VALUES #{list(node.rows)}"
108
+ def print_values_row(node) = "(#{list(node.values)})"
109
+
110
+ def print_result_column(node) = join(print(node.result), alias_of(node))
111
+ def print_star_result_column(node) = node.table ? "#{identifier(node.table)}.*" : "*"
112
+
113
+ def print_window_definition(node) = "#{identifier(node.name)} AS (#{window_body(node)})"
114
+
115
+ def window_body(node)
116
+ join(
117
+ node.base_window_name && identifier(node.base_window_name),
118
+ node.partitions && "PARTITION BY #{list(node.partitions)}",
119
+ node.orderings && "ORDER BY #{list(node.orderings)}",
120
+ print(node.frame),
121
+ )
122
+ end
123
+
124
+ def print_insert_statement(node)
125
+ verb = node.or_action ? ((node.insert_kw_tok == :REPLACE) ? "REPLACE" : "INSERT OR #{node.or_action}") : "INSERT"
126
+ join(
127
+ print(node.with_clause), verb, "INTO", print(node.table),
128
+ node.columns && "(#{list(node.columns)})",
129
+ node.default_values ? "DEFAULT VALUES" : print(node.select),
130
+ node.upserts&.map { |u| print(u) },
131
+ print(node.returning),
132
+ )
133
+ end
134
+
135
+ def print_upsert_clause(node)
136
+ target = node.targets && "(#{list(node.targets)})"
137
+ action =
138
+ if node.action == :NOTHING
139
+ "DO NOTHING"
140
+ else
141
+ join("DO UPDATE SET", list(node.assignments), node.condition && "WHERE #{print(node.condition)}")
142
+ end
143
+ join("ON CONFLICT", target, node.target_condition && "WHERE #{print(node.target_condition)}", action)
144
+ end
145
+
146
+ def print_update_statement(node)
147
+ join(
148
+ print(node.with_clause), node.or_action ? "UPDATE OR #{node.or_action}" : "UPDATE", print(node.table),
149
+ "SET", list(node.assignments),
150
+ node.source && "FROM #{print(node.source)}",
151
+ node.condition && "WHERE #{print(node.condition)}",
152
+ print(node.returning), order_and_limit(node),
153
+ )
154
+ end
155
+
156
+ def print_delete_statement(node)
157
+ join(
158
+ print(node.with_clause), "DELETE FROM", print(node.table),
159
+ node.condition && "WHERE #{print(node.condition)}",
160
+ print(node.returning), order_and_limit(node),
161
+ )
162
+ end
163
+
164
+ def print_assignment(node)
165
+ target = (node.columns.size == 1 && !node.columns_lp?) ? print(node.columns[0]) : "(#{list(node.columns)})"
166
+ "#{target} = #{print(node.value)}"
167
+ end
168
+
169
+ def print_returning_clause(node) = "RETURNING #{list(node.columns)}"
170
+
171
+ def print_create_table_statement(node)
172
+ head = join("CREATE", node.temporary && "TEMP", "TABLE", node.if_not_exists && "IF NOT EXISTS", print(node.table))
173
+ return join(head, "AS", print(node.select_statement)) if node.select_statement
174
+
175
+ definitions = list([*node.columns, *node.constraints])
176
+ join(head, "(#{definitions})", node.options && list(node.options))
177
+ end
178
+
179
+ def print_create_view_statement(node)
180
+ join(
181
+ "CREATE", node.temporary && "TEMP", "VIEW", node.if_not_exists && "IF NOT EXISTS", print(node.view),
182
+ node.columns && "(#{list(node.columns)})", "AS", print(node.select),
183
+ )
184
+ end
185
+
186
+ def print_create_index_statement(node)
187
+ join(
188
+ "CREATE", node.unique && "UNIQUE", "INDEX", node.if_not_exists && "IF NOT EXISTS", print(node.index),
189
+ "ON", identifier(node.table), "(#{list(node.columns)})",
190
+ node.condition && "WHERE #{print(node.condition)}",
191
+ )
192
+ end
193
+
194
+ def print_create_virtual_table_statement(node)
195
+ arguments = node.arguments_lp? ? "(#{node.arguments.join(", ")})" : nil
196
+ join("CREATE VIRTUAL TABLE", node.if_not_exists && "IF NOT EXISTS", print(node.table), "USING", identifier(node.module_name) + arguments.to_s)
197
+ end
198
+
199
+ def print_create_trigger_statement(node)
200
+ event = node.columns ? "UPDATE OF #{list(node.columns)}" : node.event.to_s
201
+ join(
202
+ "CREATE", node.temporary && "TEMP", "TRIGGER", node.if_not_exists && "IF NOT EXISTS", print(node.trigger),
203
+ node.time&.to_s&.tr("_", " "), event, "ON", print(node.table),
204
+ node.for_each_row_kw? && "FOR EACH ROW", node.condition && "WHEN #{print(node.condition)}",
205
+ "BEGIN", node.statements.map { |s| "#{print(s)};" }, "END",
206
+ )
207
+ end
208
+
209
+ def print_alter_table_statement(node)
210
+ action =
211
+ case node.action
212
+ when :RENAME_TABLE then "RENAME TO #{identifier(node.new_name)}"
213
+ when :RENAME_COLUMN then "RENAME COLUMN #{identifier(node.column)} TO #{identifier(node.new_name)}"
214
+ when :ADD_COLUMN then "ADD COLUMN #{print(node.column_definition)}"
215
+ when :DROP_COLUMN then "DROP COLUMN #{identifier(node.column)}"
216
+ end
217
+ join("ALTER TABLE", print(node.table), action)
218
+ end
219
+
220
+ def print_drop_statement(node) = join("DROP", node.type.to_s, node.if_exists && "IF EXISTS", print(node.target))
221
+
222
+ def print_begin_statement(node) = join("BEGIN", node.type&.to_s)
223
+ def print_commit_statement(_node) = "COMMIT"
224
+ def print_rollback_statement(node) = join("ROLLBACK", node.savepoint && "TO SAVEPOINT #{identifier(node.savepoint)}")
225
+ def print_savepoint_statement(node) = "SAVEPOINT #{identifier(node.name)}"
226
+ def print_release_statement(node) = "RELEASE SAVEPOINT #{identifier(node.name)}"
227
+
228
+ def print_pragma_statement(node)
229
+ name = qualified(node.schema, node.name)
230
+ return "PRAGMA #{name}" if node.value.nil?
231
+
232
+ value =
233
+ case node.value
234
+ when Numeric then node.value.to_s
235
+ else (node.value_tk_tok == :STRING) ? string(node.value) : identifier(node.value)
236
+ end
237
+ (node.eq_tk_tok == :LP) ? "PRAGMA #{name}(#{value})" : "PRAGMA #{name} = #{value}"
238
+ end
239
+
240
+ def print_attach_statement(node)
241
+ join("ATTACH DATABASE", print(node.file), "AS", print(node.schema), node.key && "KEY #{print(node.key)}")
242
+ end
243
+
244
+ def print_detach_statement(node) = "DETACH DATABASE #{print(node.schema)}"
245
+ def print_analyze_statement(node) = join("ANALYZE", print(node.target))
246
+ def print_reindex_statement(node) = join("REINDEX", print(node.target))
247
+ def print_vacuum_statement(node) = join("VACUUM", node.schema && identifier(node.schema), node.into && "INTO #{print(node.into)}")
248
+
249
+ # -- FROM
250
+
251
+ def print_table_name(node)
252
+ indexed = node.indexed_by ? "INDEXED BY #{identifier(node.indexed_by)}" : (node.not_indexed && "NOT INDEXED")
253
+ join(qualified(node.schema, node.table), alias_of(node), indexed)
254
+ end
255
+
256
+ def print_table_function(node)
257
+ join("#{qualified(node.schema, node.function)}(#{list(node.arguments)})", alias_of(node))
258
+ end
259
+
260
+ def print_subquery_source(node) = join("(#{print(node.select)})", alias_of(node))
261
+ def print_parenthetical_source(node) = join("(#{print(node.source)})", alias_of(node))
262
+
263
+ def print_join_clause(node)
264
+ node.joins.reduce(print(node.left)) do |sql, each|
265
+ operator = print(each.operator)
266
+ right = join(print(each.source), print(each.constraint))
267
+ (operator == ",") ? "#{sql}, #{right}" : "#{sql} #{operator} #{right}"
268
+ end
269
+ end
270
+
271
+ def print_join_operator(node) = join(node.natural && "NATURAL", JOIN_KINDS.fetch(node.kind))
272
+ def print_join_on_constraint(node) = "ON #{print(node.expression)}"
273
+ def print_join_using_constraint(node) = "USING (#{list(node.columns)})"
274
+
275
+ # -- expressions
276
+
277
+ def print_column_name(node) = qualified(node.schema, node.table, node.column)
278
+
279
+ def print_literal_expression(node)
280
+ # Numbers keep their spelling, so 0x1F stays hex and 1e3 stays an exponent
281
+ return node.value_tk_src if node.value_tk? && node.value_tk_spn.last == :numeric
282
+
283
+ case (value = node.value)
284
+ when nil then "NULL"
285
+ when true then "TRUE"
286
+ when false then "FALSE"
287
+ when Symbol then value.to_s
288
+ when Integer then value.to_s
289
+ when Float
290
+ # SQLite has no literal for NaN or infinity
291
+ value.finite? ? value.to_s : raise(ArgumentError, "cannot print #{value}")
292
+ when String
293
+ (value.encoding == Encoding::BINARY) ? "X'#{value.unpack1("H*").upcase}'" : string(value)
294
+ # A Rational would print as integer division (1/3), a Complex as nothing SQL can read
295
+ else raise ArgumentError, "cannot print #{value.inspect}"
296
+ end
297
+ end
298
+
299
+ def print_variable(node) = node.name
300
+
301
+ def print_binary_expression(node)
302
+ "#{operand(node.left)} #{BINARY_OPERATORS.fetch(node.operator)} #{operand(node.right)}"
303
+ end
304
+
305
+ def print_unary_expression(node)
306
+ case node.operator
307
+ when :NOT then "NOT #{operand(node.operand)}"
308
+ when :NEGATE then "-#{unary_operand(node.operand)}"
309
+ when :IDENTITY then "+#{unary_operand(node.operand)}"
310
+ when :INVERT then "~#{unary_operand(node.operand)}"
311
+ when :IS_NULL then "#{operand(node.operand)} ISNULL"
312
+ when :NOT_NULL then "#{operand(node.operand)} NOTNULL"
313
+ end
314
+ end
315
+
316
+ # Two minus signs in a row would start a comment
317
+ def unary_operand(node)
318
+ sql = operand(node)
319
+ sql.start_with?("-", "+") ? "(#{sql})" : sql
320
+ end
321
+
322
+ def print_ternary_expression(node)
323
+ operator = (node.operator == :NOT_BETWEEN) ? "NOT BETWEEN" : "BETWEEN"
324
+ "#{operand(node.left)} #{operator} #{operand(node.middle)} AND #{operand(node.right)}"
325
+ end
326
+
327
+ def print_like_expression(node)
328
+ operator = (node.operator == :NOT_LIKE) ? "NOT LIKE" : "LIKE"
329
+ join("#{operand(node.left)} #{operator} #{operand(node.right)}", node.escape && "ESCAPE #{operand(node.escape)}")
330
+ end
331
+ alias_method :print_not_like_expression, :print_like_expression
332
+
333
+ def print_collation_expression(node) = "#{operand(node.expression)} COLLATE #{identifier(node.sequence)}"
334
+
335
+ def print_parenthetical_expression(node) = "(#{print(node.contents)})"
336
+
337
+ def print_cast_expression(node) = "CAST(#{print(node.expression)} AS #{print(node.as)})"
338
+
339
+ def print_case_expression(node)
340
+ join("CASE", node.predicate && print(node.predicate), list_of(node.conditions), node.else_clause && "ELSE #{print(node.else_clause)}", "END")
341
+ end
342
+
343
+ def list_of(items) = items.map { |item| print(item) }
344
+
345
+ def print_case_condition(node) = "WHEN #{print(node.predicate)} THEN #{print(node.consequence)}"
346
+
347
+ def print_function_reference(node)
348
+ join(
349
+ "#{qualified(node.schema, node.function)}(#{print(node.arguments)})",
350
+ node.within_group && "WITHIN GROUP (ORDER BY #{print(node.within_group)})",
351
+ node.filter_clause && "FILTER (WHERE #{print(node.filter_clause)})",
352
+ print(node.over_clause),
353
+ )
354
+ end
355
+
356
+ def print_function_arguments(node)
357
+ join(node.distinct && "DISTINCT", list(node.expressions), node.order_by && "ORDER BY #{list(node.order_by)}")
358
+ end
359
+
360
+ def print_star_function_argument(_node) = "*"
361
+ def print_empty_function_argument(_node) = ""
362
+
363
+ def print_over_clause(node)
364
+ return "OVER #{identifier(node.window_name)}" if node.window_name
365
+
366
+ "OVER (#{window_body(node)})"
367
+ end
368
+
369
+ def print_frame_spec(node)
370
+ type = node.type.to_s.upcase
371
+ exclude = node.exclude && "EXCLUDE #{node.exclude.to_s.upcase.tr("_", " ")}"
372
+ if node.ending_boundary
373
+ join(type, "BETWEEN", print(node.starting_boundary), "AND", print(node.ending_boundary), exclude)
374
+ else
375
+ join(type, print(node.starting_boundary), exclude)
376
+ end
377
+ end
378
+
379
+ def print_frame_boundary(node)
380
+ type = node.type.to_s.upcase.tr("_", " ")
381
+ return type if type == "CURRENT ROW"
382
+
383
+ value = node.value
384
+ join((Symbol === value || String === value) ? value.to_s.upcase : print(value), type)
385
+ end
386
+
387
+ def print_subquery_expression(node) = "(#{print(node.select)})"
388
+ def print_exists_expression(node) = "EXISTS (#{print(node.select)})"
389
+
390
+ def print_raise_expression(node)
391
+ return "RAISE(IGNORE)" if node.type == :IGNORE
392
+
393
+ "RAISE(#{node.type}, #{print(node.error_message)})"
394
+ end
395
+
396
+ def print_ordering_term(node)
397
+ join(
398
+ print(node.expression), node.collation && "COLLATE #{identifier(node.collation)}",
399
+ node.direction&.to_s&.upcase, node.nulls && "NULLS #{node.nulls.to_s.upcase}",
400
+ )
401
+ end
402
+
403
+ # -- column and table definitions
404
+
405
+ def print_column_definition(node) = join(identifier(node.name), print(node.type), list_of(Array(node.constraints)))
406
+ def print_column_type(node) = node.text
407
+
408
+ def constraint_name(node) = node.name && "CONSTRAINT #{identifier(node.name)}"
409
+
410
+ def print_primary_key_column_constraint(node)
411
+ join(constraint_name(node), "PRIMARY KEY", node.direction&.to_s, print(node.conflict_clause), node.autoincrement && "AUTOINCREMENT")
412
+ end
413
+
414
+ def print_not_null_column_constraint(node) = join(constraint_name(node), "NOT NULL", print(node.conflict_clause))
415
+ def print_null_column_constraint(node) = join(constraint_name(node), "NULL", print(node.conflict_clause))
416
+ def print_unique_column_constraint(node) = join(constraint_name(node), "UNIQUE", print(node.conflict_clause))
417
+ def print_check_column_constraint(node) = join(constraint_name(node), "CHECK (#{print(node.expression)})", print(node.conflict_clause))
418
+ def print_collate_column_constraint(node) = join(constraint_name(node), "COLLATE #{identifier(node.collation)}")
419
+ def print_foreign_key_column_constraint(node) = join(constraint_name(node), print(node.foreign_key_clause))
420
+ def print_no_op_column_constraint(node) = constraint_name(node)
421
+
422
+ def print_default_column_constraint(node)
423
+ return join(constraint_name(node), "DEFAULT #{node.value_tk_src}") if node.value_tk?
424
+
425
+ value = node.value
426
+ sql =
427
+ case value
428
+ when Node then "(#{print(value)})"
429
+ else print(LiteralExpression.new(value:))
430
+ end
431
+ join(constraint_name(node), "DEFAULT #{sql}")
432
+ end
433
+
434
+ def print_generated_as_column_constraint(node)
435
+ join(constraint_name(node), "GENERATED ALWAYS AS (#{print(node.expression)})", node.type&.to_s)
436
+ end
437
+
438
+ def print_conflict_clause(node) = "ON CONFLICT #{node.resolution}"
439
+
440
+ def print_primary_key_table_constraint(node)
441
+ join(constraint_name(node), "PRIMARY KEY (#{list(node.columns)}#{" AUTOINCREMENT" if node.autoincrement})", print(node.conflict_clause))
442
+ end
443
+
444
+ def print_unique_table_constraint(node) = join(constraint_name(node), "UNIQUE (#{list(node.columns)})", print(node.conflict_clause))
445
+ def print_check_table_constraint(node) = join(constraint_name(node), "CHECK (#{print(node.expression)})")
446
+ def print_no_op_table_constraint(node) = constraint_name(node)
447
+
448
+ def print_foreign_key_table_constraint(node)
449
+ join(constraint_name(node), "FOREIGN KEY (#{list(node.columns)})", print(node.foreign_key_clause))
450
+ end
451
+
452
+ def print_foreign_key_clause(node)
453
+ deferrable = node.deferrable_span? ? node.deferrable_span_tok : (node.deferred && "DEFERRABLE INITIALLY DEFERRED")
454
+ join(
455
+ "REFERENCES", identifier(node.table), node.columns && "(#{list(node.columns)})",
456
+ list_of(Array(node.actions)), list_of(Array(node.match_clauses)),
457
+ deferrable && Array(deferrable).join(" "),
458
+ )
459
+ end
460
+
461
+ def print_foreign_key_action(node) = "ON #{node.trigger.to_s.upcase} #{FOREIGN_KEY_ACTIONS.fetch(node.action)}"
462
+ def print_match_clause(node) = "MATCH #{node.type}"
463
+
464
+ def print_indexed_column(node)
465
+ join(identifier(node.name), node.collation && "COLLATE #{identifier(node.collation)}", node.direction&.to_s)
466
+ end
467
+
468
+ def print_strict_table_option(_node) = "STRICT"
469
+ def print_without_rowid_table_option(_node) = "WITHOUT ROWID"
470
+ end
471
+ 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,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # Literal's union looks every value up in a Set of its plain values before trying its types,
5
+ # and from Ruby 4 on Set#include? hashes the value even when the set is empty. Hashing a node
6
+ # (or an array of them) hashes the whole subtree, so type-checking a tree as it is built would
7
+ # be quadratic. This union only looks up values that could be one of the plain ones
8
+ class Union
9
+ PLAIN = [String, Symbol, Integer, Float, Rational, Complex, TrueClass, FalseClass, NilClass].freeze
10
+
11
+ def initialize(*members)
12
+ plain, types = members.partition { |member| PLAIN.include?(member.class) }
13
+ @plain = plain.to_set.freeze
14
+ @types = types.freeze
15
+ freeze
16
+ end
17
+
18
+ def ===(value)
19
+ (PLAIN.include?(value.class) && @plain.include?(value)) || @types.any? { |type| type === value }
20
+ end
21
+
22
+ def inspect = "_Union(#{[*@plain, *@types].map(&:inspect).join(", ")})"
23
+
24
+ module Types
25
+ def _Union(*members) = Union.new(*members)
26
+ end
27
+ end
28
+ end
data/lib/plume/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plume
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end