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,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # Static analysis on top of the AST. The first question it answers is "which tables and
5
+ # columns would this statement read?", see `Plume.reads`; the second is what it would
6
+ # write, see `Plume.writes`.
7
+ module Analysis
8
+ autoload :Resolver, "plume/analysis/resolver"
9
+
10
+ # A read or write of one column, or of a table as a whole when `column` is nil - which is
11
+ # what `SELECT count(*) FROM t` or `DELETE FROM t` amount to.
12
+ #
13
+ # For reads, `usage` is `:projected` when the value itself can leave the statement - in the
14
+ # result rows, through RETURNING, or copied into a column being written - and `:referenced`
15
+ # when it is only used to filter, join, sort or group. For writes it is `:insert`,
16
+ # `:update` or `:delete`.
17
+ Reference = Data.define(:schema, :table, :column, :usage, :node) do
18
+ def table_key = Analysis.table_key(schema, table)
19
+ end
20
+
21
+ Unresolved = Data.define(:name, :reason, :node) do
22
+ def message
23
+ excerpt = Analysis.excerpt(node)
24
+ excerpt ? "#{reason}: #{name}\n#{excerpt}" : "#{reason}: #{name}"
25
+ end
26
+ end
27
+
28
+ # The source line `node` sits on with a caret under it, for error messages
29
+ def self.excerpt(node)
30
+ loc = node&.location
31
+ Plume.excerpt(node.full_source, *loc) if loc && node.full_source
32
+ end
33
+
34
+ class Error < StandardError
35
+ attr_reader :unresolved
36
+
37
+ def initialize(message = nil, unresolved: [])
38
+ @unresolved = unresolved
39
+ super(message || unresolved.map(&:message).join("\n"))
40
+ end
41
+ end
42
+
43
+ # Tables in the main schema are keyed by their bare name, any other schema
44
+ # (temp, attached databases) is spelled out as "schema.table"
45
+ def self.table_key(schema, table)
46
+ (schema.nil? || schema.casecmp("main")&.zero?) ? table : "#{schema}.#{table}"
47
+ end
48
+
49
+ class References
50
+ # `ctes` are the names the statement's WITH clauses define, `aliases` maps each table alias
51
+ # to the table, view, CTE or function it stands for - all spelled as written, and none from
52
+ # inside the views read. Reads and Writes of one statement share them
53
+ attr_reader :statement, :references, :unresolved, :ctes, :aliases
54
+
55
+ def initialize(statement, unresolved = [], ctes = Set.new, aliases = {})
56
+ @statement = statement
57
+ @references = []
58
+ @unresolved = unresolved
59
+ @ctes = ctes
60
+ @aliases = aliases
61
+ end
62
+
63
+ # Every base table touched, including ones touched only as a whole
64
+ def tables(usage = nil) = @references.select { |r| usage.nil? || r.usage == usage }.to_set(&:table_key)
65
+
66
+ # `{ "table" => Set["column", …] }`, optionally narrowed to one usage
67
+ def columns(usage = nil)
68
+ @references.each_with_object({}) do |ref, acc|
69
+ next if ref.column.nil? || (usage && ref.usage != usage)
70
+
71
+ (acc[ref.table_key] ||= Set.new) << ref.column
72
+ end
73
+ end
74
+
75
+ def resolved? = @unresolved.empty?
76
+
77
+ def inspect
78
+ "#<#{self.class.name} columns=#{columns.transform_values(&:to_a).inspect} unresolved=#{@unresolved.size}>"
79
+ end
80
+ end
81
+
82
+ class Reads < References
83
+ # `functions` are the table-valued ones in FROM, `calls` every other function called -
84
+ # including those LIKE, GLOB, REGEXP, MATCH, `->`, `->>` and CURRENT_* call - as SQLite's
85
+ # authorizer reports them
86
+ attr_reader :views, :functions, :calls
87
+
88
+ def initialize(...)
89
+ super
90
+ @views = Set.new
91
+ @functions = Set.new
92
+ @calls = Set.new
93
+ end
94
+
95
+ def projected = columns(:projected)
96
+ def referenced = columns(:referenced)
97
+ end
98
+
99
+ class Writes < References
100
+ # INSERT without a column list writes every column; without a catalog that is "*"
101
+ def inserted = columns(:insert)
102
+ def updated = columns(:update)
103
+ def deleted = tables(:delete)
104
+ end
105
+
106
+ # What a run of statements reads, writes and does, as `Plume.summary` gives it. `accesses` has one
107
+ # entry per statement, the rest is merged across them
108
+ class Summary
109
+ attr_reader :statements, :accesses
110
+
111
+ def initialize(statements, accesses)
112
+ @statements = statements
113
+ @accesses = accesses
114
+ end
115
+
116
+ # Each statement's node class, like "SelectStatement" or "CreateTableStatement"
117
+ def statement_types = @statements.map { |s| s.class.name.delete_prefix("Plume::") }
118
+
119
+ # Every base table read or written, or only those touched in one way: `:projected`,
120
+ # `:referenced`, `:insert`, `:update` or `:delete`
121
+ def tables(usage = nil) = each_side.reduce(Set.new) { |all, refs| all | refs.tables(usage) }
122
+
123
+ def reads = @accesses.map(&:reads)
124
+ def writes = @accesses.map(&:writes)
125
+ def actions = @accesses.flat_map(&:actions)
126
+ def views = union(reads, :views)
127
+ def functions = union(reads, :functions)
128
+ def calls = union(reads, :calls)
129
+ def ctes = union(reads, :ctes)
130
+ def aliases = reads.reduce({}) { |all, r| all.merge(r.aliases) }
131
+ def unresolved = reads.flat_map(&:unresolved)
132
+ def resolved? = unresolved.empty?
133
+
134
+ private
135
+
136
+ def each_side = reads + writes
137
+ def union(list, name) = list.reduce(Set.new) { |all, r| all | r.public_send(name) }
138
+ end
139
+
140
+ # Something a statement does besides reading and writing rows, named as SQLite's authorizer
141
+ # names it: `action` is its action code as a symbol (`:create_table`, `:drop_temp_index`,
142
+ # `:alter_table`, `:pragma`, `:transaction`…), and `name`, `detail` and `schema` are the
143
+ # arguments the authorizer gets with it - for `:create_index`, the index, its table and the schema
144
+ Action = Data.define(:action, :name, :detail, :schema, :node)
145
+
146
+ Access = Data.define(:reads, :writes, :actions) do
147
+ def unresolved = reads.unresolved
148
+ def resolved? = reads.resolved?
149
+ end
150
+ end
151
+
152
+ module Analysis
153
+ # Which tables and columns `sql` - a single SELECT, INSERT, UPDATE or DELETE - would read.
154
+ # Instead of SQL you can pass what you already parsed: a statement node, or the array
155
+ # `Plume.parse` returns, so the same SQL does not get parsed twice.
156
+ #
157
+ # Without a catalog, columns of tables cannot be enumerated: `SELECT *` comes back as a read
158
+ # of the column "*", and an unqualified column with more than one table in scope is ambiguous.
159
+ #
160
+ # With `strict: true` (the default) anything that cannot be attributed raises
161
+ # `Plume::Analysis::Error`; with `strict: false` it is listed in `Reads#unresolved` instead.
162
+ def self.reads(sql, catalog: nil, strict: true) = access(sql, catalog:, strict:).reads
163
+
164
+ # Which tables and columns an INSERT, UPDATE or DELETE would write, with the same arguments
165
+ # and strictness as `reads`. A SELECT writes nothing. The triggers a statement fires are not looked into, so rows
166
+ # they write elsewhere - or the rows a REPLACE deletes - are not reported.
167
+ def self.writes(sql, catalog: nil, strict: true) = access(sql, catalog:, strict:).writes
168
+
169
+ # Like `reads`, but also reports what an INSERT, UPDATE or DELETE writes, and in `actions` what
170
+ # any other statement does - creating, dropping and altering things, pragmas, transactions. A
171
+ # CREATE TRIGGER is analyzed for what its WHEN and body do each time it fires, a CREATE TABLE … AS
172
+ # reads what its SELECT reads, a CREATE INDEX reads what it indexes, DROP TABLE deletes the rows
173
+ def self.access(sql, catalog: nil, strict: true)
174
+ statements = statements(sql)
175
+ raise Error, "expected exactly one statement, got #{statements.size}" unless statements.size == 1
176
+
177
+ statement = statements.first
178
+ access = begin
179
+ Resolver.new(catalog:).analyze(statement)
180
+ rescue *STACK_EXHAUSTED
181
+ # As with parsing, how deep the resolver can go depends on the stack the caller has left
182
+ raise Error, "statement nested too deeply"
183
+ end
184
+ raise Error.new(unresolved: access.unresolved) if strict && !access.resolved?
185
+
186
+ access
187
+ end
188
+
189
+ # Reads and writes for any number of statements, a migration or a whole log line - with the
190
+ # same arguments as `access`. Each statement sees the schema as the statements before it left
191
+ # it: a copy of the catalog is changed as they create, alter and drop things, and the catalog
192
+ # passed in stays as it was
193
+ def self.summary(sql, catalog: nil, strict: true)
194
+ statements = statements(sql)
195
+ catalog = catalog&.dup
196
+ accesses = statements.map do |statement|
197
+ access(statement, catalog:, strict:).tap { catalog&.apply(statement) }
198
+ end
199
+ Summary.new(statements, accesses)
200
+ end
201
+
202
+ def self.statements(sql)
203
+ case sql
204
+ when Node then [sql]
205
+ when Array then sql
206
+ else Parser.new(sql).parse
207
+ end
208
+ end
209
+ private_class_method :statements
210
+ end
211
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # **[SQLite Docs](https://www.sqlite.org/lang_altertable.html)**
5
+ #
6
+ # `action` is :RENAME_TABLE, :RENAME_COLUMN, :ADD_COLUMN, :DROP_COLUMN, :ADD_CONSTRAINT (a CHECK),
7
+ # :DROP_CONSTRAINT, :SET_NOT_NULL or :DROP_NOT_NULL
8
+ class AlterTableStatement < Node
9
+ Action = _Union(
10
+ :RENAME_TABLE, :RENAME_COLUMN, :ADD_COLUMN, :DROP_COLUMN,
11
+ :ADD_CONSTRAINT, :DROP_CONSTRAINT, :SET_NOT_NULL, :DROP_NOT_NULL,
12
+ )
13
+
14
+ token :alter_table_kw
15
+ token :action_kw # RENAME [COLUMN], ADD [COLUMN], DROP [COLUMN], DROP CONSTRAINT or ALTER [COLUMN]
16
+ token :column_tk
17
+ token :to_kw
18
+ token :new_name_tk
19
+ token :constraint_name_tk
20
+ token :not_null_kw # SET NOT NULL or DROP NOT NULL
21
+
22
+ node :table, TableName
23
+ attribute :action, Action
24
+ node :column_definition, ColumnDefinition
25
+ node :constraint, CheckTableConstraint
26
+ node :conflict_clause, ConflictClause
27
+ attribute :column, Stringy
28
+ attribute :new_name, Stringy
29
+ attribute :constraint_name, Stringy
30
+
31
+ def self.new(*, table:, action:, **) = super
32
+ def self.concrete(*, alter_table_kw:, table:, action:, **) = super
33
+
34
+ def action = @action
35
+ def column = (@column == LiteralNil) ? column_tk_val : @column
36
+ def new_name = (@new_name == LiteralNil) ? new_name_tk_val : @new_name
37
+ # The constraint dropped, or added - which keeps its name on `constraint`
38
+ def constraint_name = (@constraint_name == LiteralNil) ? (constraint_name_tk_val || constraint&.name) : @constraint_name
39
+ end
40
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # **[SQLite Docs](https://www.sqlite.org/lang_analyze.html)**
5
+ #
6
+ # `target` names a schema, table or index - SQLite works out which at run time
7
+ class AnalyzeStatement < Node
8
+ token :analyze_kw
9
+
10
+ node :target, TableName
11
+
12
+ def self.new(*, **) = super
13
+ def self.concrete(*, analyze_kw:, **) = super
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # One `col = expr` or `(a, b) = expr` from a SET list
5
+ class Assignment < Node
6
+ token :columns_lp
7
+ token :columns_rp
8
+ token :eq_tk
9
+
10
+ nodes :columns, ColumnName
11
+ node :value, Expression
12
+
13
+ def self.new(*, columns:, value:, **) = super
14
+ def self.concrete(*, columns:, value:, **) = super
15
+
16
+ def column_names = columns.map(&:column)
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # **[SQLite Docs](https://www.sqlite.org/lang_attach.html)**
5
+ class AttachStatement < Node
6
+ token :attach_kw # ATTACH or ATTACH DATABASE
7
+ token :as_kw
8
+ token :key_kw
9
+
10
+ node :file, Expression
11
+ node :schema, Expression
12
+ node :key, Expression
13
+
14
+ def self.new(*, file:, schema:, **) = super
15
+ def self.concrete(*, attach_kw:, file:, schema:, **) = super
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # **[SQLite Docs](https://www.sqlite.org/lang_transaction.html)**
5
+ class BeginStatement < Node
6
+ token :begin_kw
7
+ token :type_kw
8
+ token :transaction_kw
9
+ token :name_tk
10
+
11
+ def self.new(*, **) = super
12
+ def self.concrete(*, begin_kw:, **) = super
13
+
14
+ # :DEFERRED, :IMMEDIATE, :EXCLUSIVE, or nil for SQLite's default (deferred)
15
+ inspectable def type = type_kw_tok
16
+ end
17
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class BinaryExpression < Node
5
+ TOKEN_TO_OPERATOR = {
6
+ :AND => :ALL,
7
+ :OR => :ANY,
8
+ :CONCAT => :CONCAT,
9
+ :PTR1 => :EXTRACT,
10
+ :PTR2 => :RETRIEVE,
11
+ :STAR => :MULTIPLY,
12
+ :SLASH => :DIVIDE,
13
+ :REM => :MODULO,
14
+ :PLUS => :ADD,
15
+ :MINUS => :SUB,
16
+ :BITAND => :BIT_AND,
17
+ :BITOR => :BIT_OR,
18
+ :LSHIFT => :BIT_LSHIFT,
19
+ :RSHIFT => :BIT_RSHIFT,
20
+ :LT => :BELOW,
21
+ :GT => :ABOVE,
22
+ :LE => :ATMOST,
23
+ :GE => :ATLEAST,
24
+ :EQ => :EQUALS,
25
+ :NE => :NOT_EQUALS,
26
+ :IS => :IS,
27
+ :GLOB => :GLOB,
28
+ :REGEXP => :REGEXP,
29
+ :MATCH => :MATCH,
30
+ :IN => :IN,
31
+ [:IS, :NOT, :DISTINCT, :FROM].freeze => :IS,
32
+ [:IS, :DISTINCT, :FROM].freeze => :IS_NOT,
33
+ [:IS, :NOT].freeze => :IS_NOT,
34
+ [:IS].freeze => :IS,
35
+ [:GLOB].freeze => :GLOB,
36
+ [:REGEXP].freeze => :REGEXP,
37
+ [:MATCH].freeze => :MATCH,
38
+ [:IN].freeze => :IN,
39
+ [:NOT, :GLOB].freeze => :NOT_GLOB,
40
+ [:NOT, :REGEXP].freeze => :NOT_REGEXP,
41
+ [:NOT, :MATCH].freeze => :NOT_MATCH,
42
+ [:NOT, :IN].freeze => :NOT_IN,
43
+ }.freeze
44
+ Operator = _Union(*TOKEN_TO_OPERATOR.values)
45
+
46
+ token :operator_tk # required
47
+ token :left_tk # required
48
+ token :right_tk # required
49
+
50
+ attribute :operator, Operator # required
51
+ attribute :left, Expression # required
52
+ attribute :right, Expression # required
53
+
54
+ def self.new(*, operator:, left:, right:, **) = super
55
+ def self.concrete(*, operator_tk:, **) = super
56
+
57
+ # Parser may pass either tokens or attributes, so
58
+ # require none in `concrete` signature but ensure presence here.
59
+ def after_initialize
60
+ if @left.nil? && @left_tk.nil?
61
+ raise ArgumentError.new("missing either: :left, :left_tk")
62
+ elsif @right.nil? && @right_tk.nil?
63
+ raise ArgumentError.new("missing either: :right, :right_tk")
64
+ end
65
+ end
66
+
67
+ def operator = (@operator == LiteralNil) ? TOKEN_TO_OPERATOR[operator_tk_tok] : @operator
68
+ def left = (@left == LiteralNil) ? left_tk_val : @left
69
+ def right = (@right == LiteralNil) ? right_tk_val : @right
70
+ end
71
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class CaseCondition < Node
5
+ token :when_kw
6
+ token :predicate_tk
7
+ token :then_kw
8
+ token :consequence_tk
9
+
10
+ attribute :predicate, Expression
11
+ attribute :consequence, Expression
12
+
13
+ def self.new(*, predicate:, consequence:, **) = super
14
+ def self.concrete(*, when_kw:, then_kw:, **) = super
15
+
16
+ # Parser may pass either tokens or attributes, so
17
+ # require none in `concrete` signature but ensure presence here.
18
+ def after_initialize
19
+ if @predicate.nil? && @predicate_tk.nil?
20
+ raise ArgumentError.new("missing either: :predicate, :predicate_tk")
21
+ elsif @consequence.nil? && @consequence_tk.nil?
22
+ raise ArgumentError.new("missing either: :consequence, :consequence_tk")
23
+ end
24
+ end
25
+
26
+ def predicate = (@predicate == LiteralNil) ? predicate_tk_val : @predicate
27
+ def consequence = (@consequence == LiteralNil) ? consequence_tk_val : @consequence
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class CaseExpression < Node
5
+ token :case_kw
6
+ token :predicate_tk
7
+ token :else_kw
8
+ token :else_clause_tk
9
+ token :end_kw
10
+
11
+ attribute :predicate, Expression
12
+ attribute :else_clause, Expression
13
+
14
+ nodes :conditions, CaseCondition
15
+
16
+ def self.new(*, conditions:, **) = super
17
+ def self.concrete(*, case_kw:, conditions:, end_kw:, **) = super
18
+
19
+ def predicate = (@predicate == LiteralNil) ? predicate_tk_val : @predicate
20
+ def else_clause = (@else_clause == LiteralNil) ? else_clause_tk_val : @else_clause
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class CastExpression < Node
5
+ token :cast_kw
6
+ token :expression_lp
7
+ token :as_kw
8
+ token :expression_rp
9
+
10
+ node :expression, Expression
11
+ node :as, ColumnType
12
+
13
+ def self.new(*, expression:, as:, **) = super
14
+ def self.concrete(*, cast_kw:, expression_lp:, expression:, as_kw:, as:, expression_rp:, **) = super
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class CheckColumnConstraint < ColumnConstraint
5
+ token :constraint_kw
6
+ token :name_tk
7
+ token :check_kw
8
+ token :check_lp
9
+ token :check_rp
10
+
11
+ attribute :name, Stringy
12
+ node :expression, Expression
13
+ node :conflict_clause, ConflictClause
14
+
15
+ def self.new(*, expression:, **) = super
16
+ def self.concrete(*, check_kw:, check_lp:, expression:, check_rp:, **) = super
17
+
18
+ def name = (@name == LiteralNil) ? name_tk_val : @name
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class CheckTableConstraint < TableConstraint
5
+ token :constraint_kw
6
+ token :name_tk
7
+ token :check_kw
8
+ token :check_lp
9
+ token :check_rp
10
+
11
+ attribute :name, Stringy
12
+ node :expression, Expression
13
+ node :conflict_clause, ConflictClause
14
+
15
+ def self.new(*, expression:, **) = super
16
+ def self.concrete(*, expression:, **) = super
17
+
18
+ def name = (@name == LiteralNil) ? name_tk_val : @name
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class CollateColumnConstraint < ColumnConstraint
5
+ token :constraint_kw
6
+ token :name_tk
7
+ token :collate_kw
8
+ token :collation_tk
9
+
10
+ attribute :name, Stringy
11
+ attribute :collation, Stringy
12
+
13
+ def self.new(*, collation:, **) = super
14
+ def self.concrete(*, collate_kw:, collation_tk:, **) = super
15
+
16
+ def name = (@name == LiteralNil) ? name_tk_val : @name
17
+ def collation = (@collation == LiteralNil) ? Node.collation_name(collation_tk_val) : @collation
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class CollationExpression < Node
5
+ token :collate_kw
6
+ token :expression_tk
7
+ token :sequence_tk
8
+
9
+ attribute :expression, Expression
10
+ attribute :sequence, Stringy
11
+
12
+ def self.new(*, sequence:, **) = super
13
+ def self.concrete(*, sequence_tk:, **) = super
14
+
15
+ # Parser may pass either tokens or attributes, so
16
+ # require none in `concrete` signature but ensure presence here.
17
+ def after_initialize
18
+ if @expression.nil? && @expression_tk.nil?
19
+ raise ArgumentError.new("missing either: :expression, :expression_tk")
20
+ end
21
+ end
22
+
23
+ def expression = (@expression == LiteralNil) ? expression_tk_val : @expression
24
+ def sequence = (@sequence == LiteralNil) ? Node.collation_name(sequence_tk_val) : @sequence
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class ColumnConstraint < Node
5
+ # abstract
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class ColumnDefinition < Node
5
+ token :name_tk
6
+
7
+ attribute :name, Stringy
8
+ node :type, ColumnType
9
+ nodes :constraints, ColumnConstraint
10
+
11
+ def self.new(*, name:, **) = super
12
+ def self.concrete(*, name_tk:, **) = super
13
+
14
+ def name = (@name == LiteralNil) ? name_tk_val : @name
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class ColumnName < Node
5
+ token :schema_tk # maybe
6
+ token :dot1 # maybe
7
+ token :table_tk # maybe
8
+ token :dot2 # maybe
9
+ token :column_tk # required
10
+
11
+ attribute :schema, Stringy # maybe
12
+ attribute :table, Stringy # maybe
13
+ attribute :column, Stringy # required
14
+
15
+ def self.new(*, column:, **) = super
16
+ def self.concrete(*, column_tk:, **) = super
17
+
18
+ def schema = (@schema == LiteralNil) ? schema_tk_val : @schema
19
+ def table = (@table == LiteralNil) ? table_tk_val : @table
20
+ def column = (@column == LiteralNil) ? column_tk_val : @column
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class ColumnType < Node
5
+ token :text_span
6
+
7
+ attribute :text, Stringy
8
+
9
+ def self.new(*, text:, **) = super
10
+ def self.concrete(*, text_span:, **) = super
11
+
12
+ def text = (@text == LiteralNil) ? text_span_val : @text
13
+
14
+ inspectable def affinity
15
+ # rules found at: https://www.sqlite.org/datatype3.html#determination_of_column_affinity
16
+ # Matched as bytes, as SQLite does, so a type name that is not valid UTF-8 has one too
17
+ case text&.b
18
+ when /INT/i then :INTEGER
19
+ when /CHAR|CLOB|TEXT/i then :TEXT
20
+ when /BLOB/i then :BLOB
21
+ when /REAL|FLOA|DOUB/i then :REAL
22
+ else :ANY
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # COMMIT or END. **[SQLite Docs](https://www.sqlite.org/lang_transaction.html)**
5
+ class CommitStatement < Node
6
+ token :commit_kw
7
+ token :transaction_kw
8
+ token :name_tk
9
+
10
+ def self.new(*, **) = super
11
+ def self.concrete(*, commit_kw:, **) = super
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class CommonTableExpression < Node
5
+ token :name_tk
6
+ token :columns_lp
7
+ token :columns_rp
8
+ token :as_kw
9
+ token :materialized_tk
10
+ token :select_lp
11
+ token :select_rp
12
+
13
+ attribute :name, Stringy
14
+ nodes :columns, IndexedColumn
15
+ node :select, SelectStatement
16
+
17
+ def self.new(*, name:, select:, **) = super
18
+ def self.concrete(*, name_tk:, select:, **) = super
19
+
20
+ def name = (@name == LiteralNil) ? name_tk_val : @name
21
+ def column_names = columns&.map(&:name)
22
+
23
+ # true for MATERIALIZED, false for NOT MATERIALIZED, nil when left to the planner
24
+ inspectable def materialized
25
+ case materialized_tk_tok
26
+ when :MATERIALIZED then true
27
+ when [:NOT, :MATERIALIZED] then false
28
+ end
29
+ end
30
+ end
31
+ end