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,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class GeneratedAsColumnConstraint < ColumnConstraint
5
+ Type = _Union(:STORED, :VIRTUAL)
6
+
7
+ token :constraint_kw
8
+ token :name_tk
9
+ token :as_kw
10
+ token :as_lp
11
+ token :as_rp
12
+ token :type_tk
13
+
14
+ attribute :name, Stringy
15
+ attribute :type, Type
16
+ node :expression, Expression
17
+
18
+ def self.new(*, expression:, **) = super
19
+ def self.concrete(*, as_kw:, as_lp:, expression:, as_rp:, **) = super
20
+
21
+ def name = (@name == LiteralNil) ? name_tk_val : @name
22
+ def type = (@type == LiteralNil) ? type_tk_val&.to_sym&.upcase : @type
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class IndexedColumn < Node
5
+ token :name_tk
6
+ token :collate_kw
7
+ token :collation_tk
8
+ token :direction_tk
9
+ token :nulls_tk
10
+
11
+ attribute :name, Stringy
12
+ attribute :collation, Stringy
13
+ attribute :direction, Stringy
14
+ attribute :nulls, OrderingTerm::Nulls
15
+
16
+ def self.new(*, name:, **) = super
17
+ def self.concrete(*, name_tk:, **) = super
18
+
19
+ def name = (@name == LiteralNil) ? name_tk_val : @name
20
+ def collation = (@collation == LiteralNil) ? Node.collation_name(collation_tk_val) : @collation
21
+ def direction = (@direction == LiteralNil) ? direction_tk_val&.to_sym&.upcase : @direction
22
+ def nulls = (@nulls == LiteralNil) ? nulls_tk_tok&.last : @nulls
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # **[SQLite Docs](https://www.sqlite.org/lang_insert.html)**
5
+ #
6
+ # ```sql
7
+ # insert or ignore into t (a, b) select x, y from u on conflict (a) do nothing returning *
8
+ # ```
9
+ #
10
+ # Rows come from `select` (a SelectStatement, VALUES included) or are DEFAULT VALUES.
11
+ class InsertStatement < Node
12
+ include ConflictResolution
13
+
14
+ token :insert_kw # INSERT [OR …], or REPLACE
15
+ token :into_kw
16
+ token :columns_lp
17
+ token :columns_rp
18
+ token :default_values_kw
19
+
20
+ node :with_clause, WithClause
21
+ node :table, TableName
22
+ nodes :columns, ColumnName
23
+ node :select, SelectStatement
24
+ nodes :upserts, UpsertClause
25
+ node :returning, ReturningClause
26
+
27
+ def self.new(*, table:, **) = super
28
+ def self.concrete(*, insert_kw:, into_kw:, table:, **) = super
29
+
30
+ inspectable def or_action = TOKEN_TO_RESOLUTION[insert_kw_tok]
31
+ inspectable def default_values = default_values_kw?
32
+
33
+ def column_names = columns&.map(&:column)
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class Join < Node
5
+ node :operator, JoinOperator
6
+ node :source, _Any
7
+ node :constraint, _Union(JoinOnConstraint, JoinUsingConstraint)
8
+
9
+ def self.new(*, operator:, source:, **) = super
10
+ def self.concrete(*, operator:, source:, **) = super
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # `left` followed by one or more joins. Comma-separated sources are joins too,
5
+ # with a `:COMMA` operator
6
+ class JoinClause < Node
7
+ node :left, _Any
8
+ nodes :joins, Join
9
+
10
+ def self.new(*, left:, joins:, **) = super
11
+ def self.concrete(*, left:, joins:, **) = super
12
+
13
+ def sources = [left, *joins.map(&:source)]
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class JoinOnConstraint < Node
5
+ token :on_kw
6
+
7
+ node :expression, Expression
8
+
9
+ def self.new(*, expression:, **) = super
10
+ def self.concrete(*, on_kw:, expression:, **) = super
11
+ end
12
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class JoinOperator < Node
5
+ Kind = _Union(:COMMA, :INNER, :LEFT, :RIGHT, :FULL, :CROSS)
6
+
7
+ token :operator_tk
8
+
9
+ attribute :kind, Kind
10
+ attribute :natural, _Boolean
11
+
12
+ def self.new(*, kind:, **) = super
13
+ def self.concrete(*, operator_tk:, **) = super
14
+
15
+ def kind
16
+ return @kind unless @kind == LiteralNil
17
+
18
+ keywords = operator_keywords
19
+ return :COMMA if keywords == [:COMMA]
20
+
21
+ (keywords & [:LEFT, :RIGHT, :FULL, :CROSS]).first || :INNER
22
+ end
23
+
24
+ def natural = (@natural == LiteralNil) ? operator_keywords.include?(:NATURAL) : @natural
25
+ def outer? = [:LEFT, :RIGHT, :FULL].include?(kind)
26
+
27
+ private
28
+
29
+ def operator_keywords = Array(operator_tk_tok)
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class JoinUsingConstraint < Node
5
+ token :using_kw
6
+ token :columns_lp
7
+ token :columns_rp
8
+
9
+ nodes :columns, ColumnName
10
+
11
+ def self.new(*, columns:, **) = super
12
+ def self.concrete(*, using_kw:, columns:, **) = super
13
+
14
+ def column_names = columns.map(&:column)
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # `left [NOT] LIKE right [ESCAPE escape]`. GLOB, REGEXP and MATCH have no ESCAPE
5
+ # and are plain `BinaryExpression`s
6
+ class LikeExpression < Node
7
+ token :operator_tk
8
+ token :escape_kw
9
+
10
+ node :left, Expression
11
+ node :right, Expression
12
+ node :escape, Expression
13
+
14
+ def self.new(*, left:, right:, **) = super
15
+ def self.concrete(*, operator_tk:, left:, right:, **) = super
16
+
17
+ def operator = :LIKE
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class LiteralExpression < Node
5
+ token :value_tk # required
6
+
7
+ attribute :value, _Void # required
8
+
9
+ def self.new(*, value:, **) = super
10
+ def self.concrete(*, **) = super
11
+
12
+ # Parser may pass either tokens or attributes, so
13
+ # require none in `concrete` signature but ensure presence here.
14
+ def after_initialize
15
+ if @value == LiteralNil && @value_tk == LiteralNil
16
+ raise ArgumentError.new("missing either: :value, :value_tk")
17
+ end
18
+ end
19
+
20
+ def value
21
+ v = (@value == LiteralNil) ? value_tk_val : @value
22
+ case v
23
+ when LiteralNil then nil
24
+ when LiteralFalse then false
25
+ else v
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ LiteralFalse = Object.new
5
+ def LiteralFalse.inspect = "<LiteralFalse>"
6
+ LiteralFalse.freeze
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ LiteralNil = Object.new
5
+ def LiteralNil.inspect = "<LiteralNil>"
6
+ LiteralNil.freeze
7
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ LiteralValue = _Union(
5
+ nil,
6
+ true,
7
+ false,
8
+ :CURRENT_TIME,
9
+ :CURRENT_DATE,
10
+ :CURRENT_TIMESTAMP,
11
+ :*,
12
+ Numeric,
13
+ String,
14
+ # Blob,
15
+ LiteralNil,
16
+ LiteralFalse,
17
+ )
18
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class MatchClause < Node
5
+ token :match_kw
6
+ token :type_tk
7
+
8
+ attribute :type, Stringy
9
+
10
+ def self.new(*, type:, **) = super
11
+ def self.concrete(*, match_kw:, type_tk:, **) = super
12
+
13
+ def type = (@type == LiteralNil) ? type_tk_src.to_sym.upcase : @type
14
+ end
15
+ end
@@ -0,0 +1,330 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class Node < Literal::Struct
5
+ extend Union::Types
6
+
7
+ Stringy = _Union(String, Symbol)
8
+
9
+ def self.literal_properties
10
+ super
11
+ rescue => error
12
+ raise unless defined?(Ractor::IsolationError) && Ractor::IsolationError === error
13
+
14
+ raise Ractor::IsolationError, "#{name} is not shareable yet, call Plume.eager_load! from the main Ractor first " \
15
+ "(or #{name}.make_shareable! for a class defined after it)"
16
+ end
17
+
18
+ def self.make_shareable!
19
+ Plume.make_shareable(literal_properties)
20
+ Plume.make_shareable(@inspectable_props)
21
+ Plume.make_shareable(@token_names)
22
+ self
23
+ end
24
+
25
+ def self.prop(name, ...)
26
+ raise FrozenError, "cannot add #{name} to #{self.name} after Plume.eager_load! has frozen its props" if literal_properties.frozen?
27
+
28
+ super
29
+ end
30
+
31
+ singleton_class.prepend(LiteralPatches::NodeSchema) if LiteralPatches::SCHEMA_HOLDS_MUTEX
32
+
33
+ prop :full_source, _Nilable(String)
34
+ prop :leading, _Nilable(Token)
35
+ prop :trailing, _Nilable(Token)
36
+
37
+ # A collation's name, upcased as a symbol. A symbol cannot hold bytes that
38
+ # are not valid UTF-8, so a name with those stays the string it was
39
+ def self.collation_name(name) = name&.valid_encoding? ? name.to_sym.upcase : name
40
+
41
+ def self.inspectable(*props)
42
+ @inspectable_props ||= Set.new
43
+ @inspectable_props.merge(props)
44
+ end
45
+
46
+ def self.all_inspectable_props
47
+ ancestors.reduce(Set.new) do |all_props, ancestor|
48
+ if (props = ancestor.instance_variable_get(:@inspectable_props))
49
+ all_props.merge(props)
50
+ else
51
+ all_props
52
+ end
53
+ end
54
+ end
55
+
56
+ def inspectable_props
57
+ all_inspectable_props = self.class.all_inspectable_props
58
+ all_inspectable_props.map do |prop|
59
+ val = __send__(prop)
60
+ next if val.nil? || (val.respond_to?(:empty?) && val.empty?)
61
+
62
+ [prop, val]
63
+ end.tap(&:compact!)
64
+ end
65
+
66
+ alias_method :full_inspect, :inspect
67
+ def inspect
68
+ return "#<#{self.class.name}:#{object_id}>" if inspectable_props.empty?
69
+
70
+ prop_values = inspectable_props.map do |prop, val|
71
+ "#{prop}=#{val.inspect}"
72
+ end
73
+
74
+ "#<#{self.class.name}:#{object_id} #{prop_values&.join(", ")}>"
75
+ end
76
+
77
+ def pretty_please(pp)
78
+ pp.push "#{self.class.name}("
79
+ pp.map(inspectable_props) do |prop, val|
80
+ "#{prop} = #{pp.capture { pp.prettify(val) }}"
81
+ end
82
+ pp.push ")"
83
+ end
84
+
85
+ # Direct child nodes in property declaration order. Arrays of nodes are flattened in
86
+ def children
87
+ self.class.literal_properties.each_with_object([]) do |property, kids|
88
+ value = instance_variable_get(:"@#{property.name}")
89
+ case value
90
+ when Node then kids << value
91
+ when Array then value.each { |v| kids << v if Node === v }
92
+ end
93
+ end
94
+ end
95
+
96
+ # Yields each direct child with the prop it sits in, and its index when that prop holds an array
97
+ def each_child
98
+ # Each as one array: TruffleRuby's Enumerator hands only the first of several yielded values to `count`
99
+ return Enumerator.new { |y| each_child { |*slot| y << slot } } unless block_given?
100
+
101
+ self.class.literal_properties.each do |property|
102
+ case (value = instance_variable_get(:"@#{property.name}"))
103
+ when Node then yield value, property.name, nil
104
+ when Array then value.each_with_index { |v, i| yield v, property.name, i if Node === v }
105
+ end
106
+ end
107
+ self
108
+ end
109
+
110
+ # Yields every node in the subtree depth-first, parents before their children,
111
+ # along with the parent (nil for the receiver). Without recursing, so a tree as
112
+ # deep as SQLite allows does not run out of stack
113
+ def walk(parent = nil)
114
+ return enum_for(:walk, parent) unless block_given?
115
+
116
+ stack = [[self, parent]]
117
+ while (node, up = stack.pop)
118
+ yield node, up
119
+ node.children.reverse_each { |child| stack << [child, node] }
120
+ end
121
+ self
122
+ end
123
+
124
+ # Like `walk`, but also says where under the parent each node sits: the prop's name,
125
+ # and the index into it when it holds an array
126
+ def walk_with_slots(parent = nil)
127
+ return Enumerator.new { |y| walk_with_slots(parent) { |*slot| y << slot } } unless block_given?
128
+
129
+ stack = [[self, parent, nil, nil]]
130
+ while (node, up, prop, index = stack.pop)
131
+ yield node, up, prop, index
132
+ start = stack.size
133
+ node.each_child { |child, name, i| stack << [child, node, name, i] }
134
+ stack[start..] = stack[start..].reverse
135
+ end
136
+ self
137
+ end
138
+
139
+ # A copy of the tree with nodes replaced. The block sees every node as `walk_with_slots` does,
140
+ # and returns it to keep it, or another node to put in its place - which is not descended into.
141
+ # Nodes on the way to a replacement are copied, everything else is shared with the original,
142
+ # which stays as it was. A replacement built with `.new` has no source, so print the result
143
+ # with `to_sql` rather than reading its `source_text`
144
+ def transform
145
+ replaced = {}.compare_by_identity
146
+ kept = []
147
+ stack = [[self, nil, nil, nil]]
148
+ while (node, up, prop, index = stack.pop)
149
+ replacement = yield node, up, prop, index
150
+ if replacement.equal?(node)
151
+ kept << node
152
+ node.each_child.reverse_each { |child, name, i| stack << [child, node, name, i] }
153
+ else
154
+ replaced[node] = replacement
155
+ end
156
+ end
157
+
158
+ # Parents come before their children in `kept`, so going backwards rebuilds children first
159
+ kept.reverse_each do |node|
160
+ changes = {}
161
+ node.each_child do |child, name, i|
162
+ next unless replaced.key?(child)
163
+
164
+ changes[name] ||= node.instance_variable_get(:"@#{name}").dup if i
165
+ i ? changes[name][i] = replaced[child] : changes[name] = replaced[child]
166
+ end
167
+ replaced[node] = node.with_props(changes) unless changes.empty?
168
+ end
169
+ replaced.fetch(self, self)
170
+ end
171
+
172
+ # A copy with some props set to new values, checked against their types
173
+ def with_props(changes)
174
+ copy = dup
175
+ changes.each do |name, value|
176
+ property = self.class.literal_properties[name]
177
+ raise TypeError, "#{self.class.name}##{name} cannot be #{value.inspect}" unless property.type === value
178
+
179
+ copy.instance_variable_set(:"@#{name}", value)
180
+ end
181
+ copy
182
+ end
183
+
184
+ def each_node(&block)
185
+ return enum_for(:each_node) unless block
186
+
187
+ walk { |node, _parent| yield node }
188
+ end
189
+
190
+ # Byte range `[beg, fin]` of the source this node was parsed from, spanning every token
191
+ # in its subtree. Separators the node merely carries (`leading`/`trailing`) are left out.
192
+ # Nil for nodes built by hand with `.new`
193
+ def location
194
+ beg = fin = nil
195
+ each_token_span do |_tok, b, f|
196
+ beg = b if beg.nil? || b < beg
197
+ fin = f if fin.nil? || f > fin
198
+ end
199
+ [beg, fin].freeze if beg
200
+ end
201
+
202
+ # `pretty: true` lays the SQL out over lines, see PrettyPrinter for `indent_size:`, `max_line_length:`
203
+ # and `commas_start_of_line:`. With `comments: true`, the comments right before and after the node
204
+ # in its source come along - a header above a statement, or the sqlcommenter tags after it. Those
205
+ # inside it are left out. A `--` comment after it becomes a `/* */` one, so a semicolon after it is
206
+ # not swallowed
207
+ def to_sql(pretty: false, comments: false, **layout)
208
+ sql = pretty ? PrettyPrinter.print(self, **layout) : Printer.print(self)
209
+ return sql unless comments
210
+
211
+ before, after = Comments.around(self)
212
+ lead = before.map { |c| c.text + (c.block? ? " " : "\n") }.join
213
+ tail = after.map { |c| " #{((c.block? || c.body.include?("*/")) ? c.text : "/*#{c.body} */")}" }.join
214
+ lead + sql + tail
215
+ end
216
+
217
+ # A plain hash of the whole tree that JSON can carry, which `Plume.load` turns back into it
218
+ def dump = Dump.dump(self)
219
+
220
+ def source_text
221
+ return unless @full_source && (loc = location)
222
+
223
+ @full_source.byteslice(loc[0], loc[1] - loc[0])
224
+ end
225
+
226
+ def each_token_span(&block)
227
+ instance_variables.each do |ivar|
228
+ next if ivar == :@leading || ivar == :@trailing
229
+
230
+ case (value = instance_variable_get(ivar))
231
+ when Node then value.each_token_span(&block)
232
+ when Array
233
+ if Token === value
234
+ yield value
235
+ else
236
+ value.each do |v|
237
+ if Node === v then v.each_token_span(&block)
238
+ elsif Token === v then yield v
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ # Uses the public readers, so token-backed values like `TableName#table` come out
247
+ # as their parsed values rather than raw token spans
248
+ def deconstruct_keys(keys)
249
+ names = keys || self.class.literal_properties.map(&:name)
250
+ names.each_with_object({}) do |name, h|
251
+ h[name] = public_send(name) if respond_to?(name)
252
+ end
253
+ end
254
+
255
+ def self.concrete(*a, full_source:, **kw)
256
+ object = allocate
257
+ object.__send__(:initialize, *a, full_source: full_source, **kw)
258
+ object
259
+ end
260
+
261
+ # Every prop is type checked as a tree is built, so the check must never hash a node,
262
+ # see Plume::Union
263
+ def self.attribute(name, type, **kw)
264
+ prop name, _Nilable(_Union(type, LiteralNil)), reader: kw.fetch(:reader, false), default: LiteralNil, **kw
265
+ inspectable name
266
+ end
267
+
268
+ def self.token_names
269
+ ancestors.each_with_object(Set.new) do |ancestor, names|
270
+ names.merge(ancestor.instance_variable_get(:@token_names) || [])
271
+ end
272
+ end
273
+
274
+ def self.token(name, **kw)
275
+ (@token_names ||= Set.new) << name
276
+ prop name, _Nilable(_Union(Token, LiteralNil)), reader: false, default: LiteralNil, **kw
277
+
278
+ # Define methods for accessing token properties, like the original source text, parsed value, and location.
279
+ # The token reader method is simply the parsed value of the token.
280
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
281
+ # frozen_string_literal: true
282
+
283
+ def #{name}_src
284
+ Token.source(@full_source, @#{name})
285
+ end
286
+
287
+ def #{name}_val
288
+ Token.value(@full_source, @#{name})
289
+ end
290
+ alias_method :#{name}, :#{name}_val
291
+
292
+ def #{name}_loc
293
+ Token.location(@#{name})
294
+ end
295
+
296
+ def #{name}_tok
297
+ Token.token(@#{name})
298
+ end
299
+
300
+ def #{name}_spn
301
+ @#{name}
302
+ end
303
+
304
+ def #{name}?
305
+ !(@#{name}.nil? || LiteralNil.equal?(@#{name}))
306
+ end
307
+ RUBY
308
+ end
309
+
310
+ def self.node(name, type, **kw)
311
+ attribute name, _Nilable(type), **kw
312
+ define_node_reader(name)
313
+ end
314
+
315
+ def self.nodes(name, type, **kw)
316
+ attribute name, _Nilable(_Array(type)), **kw
317
+ define_node_reader(name)
318
+ end
319
+
320
+ # Absent children read as nil rather than the LiteralNil default. Evaluated from a string
321
+ # because a method defined with a block cannot be called from another Ractor
322
+ def self.define_node_reader(name)
323
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
324
+ def #{name}
325
+ LiteralNil.equal?(@#{name}) ? nil : @#{name}
326
+ end
327
+ RUBY
328
+ end
329
+ end
330
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class NoOpColumnConstraint < ColumnConstraint
5
+ token :constraint_kw
6
+ token :name_tk
7
+
8
+ attribute :name, Stringy
9
+
10
+ def self.new(*, **) = super
11
+ def self.concrete(*, **) = super
12
+
13
+ def name = (@name == LiteralNil) ? name_tk_val : @name
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class NoOpTableConstraint < TableConstraint
5
+ token :constraint_kw
6
+ token :name_tk
7
+
8
+ attribute :name, Stringy
9
+
10
+ def self.new(*, **) = super
11
+ def self.concrete(*, **) = super
12
+
13
+ def name = (@name == LiteralNil) ? name_tk_val : @name
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class NotLikeExpression < LikeExpression
5
+ def operator = :NOT_LIKE
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class NotNullColumnConstraint < ColumnConstraint
5
+ token :constraint_kw
6
+ token :name_tk
7
+ token :not_null_kw
8
+
9
+ attribute :name, Stringy
10
+ node :conflict_clause, ConflictClause
11
+
12
+ def self.new(*, **) = super
13
+ def self.concrete(*, not_null_kw:, **) = super
14
+
15
+ def name = (@name == LiteralNil) ? name_tk_val : @name
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ class NullColumnConstraint < ColumnConstraint
5
+ token :constraint_kw
6
+ token :name_tk
7
+ token :null_kw
8
+
9
+ attribute :name, Stringy
10
+ node :conflict_clause, ConflictClause
11
+
12
+ def self.new(*, **) = super
13
+ def self.concrete(*, null_kw:, **) = super
14
+
15
+ def name = (@name == LiteralNil) ? name_tk_val : @name
16
+ end
17
+ end