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.
- checksums.yaml +4 -4
- data/README.md +506 -11
- data/lib/plume/analysis/resolver.rb +797 -0
- data/lib/plume/analysis.rb +211 -0
- data/lib/plume/ast/alter_table_statement.rb +40 -0
- data/lib/plume/ast/analyze_statement.rb +15 -0
- data/lib/plume/ast/assignment.rb +18 -0
- data/lib/plume/ast/attach_statement.rb +17 -0
- data/lib/plume/ast/begin_statement.rb +17 -0
- data/lib/plume/ast/binary_expression.rb +71 -0
- data/lib/plume/ast/case_condition.rb +29 -0
- data/lib/plume/ast/case_expression.rb +22 -0
- data/lib/plume/ast/cast_expression.rb +16 -0
- data/lib/plume/ast/check_column_constraint.rb +20 -0
- data/lib/plume/ast/check_table_constraint.rb +20 -0
- data/lib/plume/ast/collate_column_constraint.rb +19 -0
- data/lib/plume/ast/collation_expression.rb +26 -0
- data/lib/plume/ast/column_constraint.rb +7 -0
- data/lib/plume/ast/column_definition.rb +16 -0
- data/lib/plume/ast/column_name.rb +22 -0
- data/lib/plume/ast/column_type.rb +26 -0
- data/lib/plume/ast/commit_statement.rb +13 -0
- data/lib/plume/ast/common_table_expression.rb +31 -0
- data/lib/plume/ast/compound_select.rb +30 -0
- data/lib/plume/ast/conflict_clause.rb +22 -0
- data/lib/plume/ast/conflict_resolution.rb +20 -0
- data/lib/plume/ast/create_index_statement.rb +29 -0
- data/lib/plume/ast/create_table_statement.rb +47 -0
- data/lib/plume/ast/create_trigger_statement.rb +43 -0
- data/lib/plume/ast/create_view_statement.rb +30 -0
- data/lib/plume/ast/create_virtual_table_statement.rb +28 -0
- data/lib/plume/ast/default_column_constraint.rb +29 -0
- data/lib/plume/ast/deferrable_column_constraint.rb +20 -0
- data/lib/plume/ast/delete_statement.rb +28 -0
- data/lib/plume/ast/detach_statement.rb +13 -0
- data/lib/plume/ast/drop_statement.rb +21 -0
- data/lib/plume/ast/empty_function_argument.rb +6 -0
- data/lib/plume/ast/exists_expression.rb +15 -0
- data/lib/plume/ast/explain_statement.rb +15 -0
- data/lib/plume/ast/expression.rb +30 -0
- data/lib/plume/ast/foreign_key_action.rb +26 -0
- data/lib/plume/ast/foreign_key_clause.rb +50 -0
- data/lib/plume/ast/foreign_key_column_constraint.rb +16 -0
- data/lib/plume/ast/foreign_key_table_constraint.rb +20 -0
- data/lib/plume/ast/frame_boundary.rb +35 -0
- data/lib/plume/ast/frame_spec.rb +34 -0
- data/lib/plume/ast/function_arguments.rb +18 -0
- data/lib/plume/ast/function_reference.rb +29 -0
- data/lib/plume/ast/generated_as_column_constraint.rb +24 -0
- data/lib/plume/ast/indexed_column.rb +24 -0
- data/lib/plume/ast/insert_statement.rb +35 -0
- data/lib/plume/ast/join.rb +12 -0
- data/lib/plume/ast/join_clause.rb +15 -0
- data/lib/plume/ast/join_on_constraint.rb +12 -0
- data/lib/plume/ast/join_operator.rb +31 -0
- data/lib/plume/ast/join_using_constraint.rb +16 -0
- data/lib/plume/ast/like_expression.rb +19 -0
- data/lib/plume/ast/literal_expression.rb +29 -0
- data/lib/plume/ast/literal_false.rb +7 -0
- data/lib/plume/ast/literal_nil.rb +7 -0
- data/lib/plume/ast/literal_value.rb +18 -0
- data/lib/plume/ast/match_clause.rb +15 -0
- data/lib/plume/ast/node.rb +330 -0
- data/lib/plume/ast/noop_column_constraint.rb +15 -0
- data/lib/plume/ast/noop_table_constraint.rb +15 -0
- data/lib/plume/ast/not_like_expression.rb +7 -0
- data/lib/plume/ast/not_null_column_constraint.rb +17 -0
- data/lib/plume/ast/null_column_constraint.rb +17 -0
- data/lib/plume/ast/ordering_term.rb +32 -0
- data/lib/plume/ast/over_clause.rb +25 -0
- data/lib/plume/ast/parenthetical_expression.rb +37 -0
- data/lib/plume/ast/parenthetical_source.rb +19 -0
- data/lib/plume/ast/pragma_statement.rb +26 -0
- data/lib/plume/ast/primary_key_column_constraint.rb +23 -0
- data/lib/plume/ast/primary_key_table_constraint.rb +23 -0
- data/lib/plume/ast/raise_expression.rb +23 -0
- data/lib/plume/ast/reindex_statement.rb +15 -0
- data/lib/plume/ast/release_statement.rb +16 -0
- data/lib/plume/ast/result_column.rb +18 -0
- data/lib/plume/ast/returning_clause.rb +12 -0
- data/lib/plume/ast/rollback_statement.rb +19 -0
- data/lib/plume/ast/savepoint_statement.rb +16 -0
- data/lib/plume/ast/select_core.rb +27 -0
- data/lib/plume/ast/select_statement.rb +34 -0
- data/lib/plume/ast/star_function_argument.rb +6 -0
- data/lib/plume/ast/star_result_column.rb +16 -0
- data/lib/plume/ast/strict_table_option.rb +6 -0
- data/lib/plume/ast/subquery_expression.rb +14 -0
- data/lib/plume/ast/subquery_source.rb +19 -0
- data/lib/plume/ast/table_constraint.rb +7 -0
- data/lib/plume/ast/table_function.rb +28 -0
- data/lib/plume/ast/table_name.rb +28 -0
- data/lib/plume/ast/table_option.rb +7 -0
- data/lib/plume/ast/ternary_expression.rb +26 -0
- data/lib/plume/ast/token.rb +183 -0
- data/lib/plume/ast/unary_expression.rb +36 -0
- data/lib/plume/ast/unique_column_constraint.rb +17 -0
- data/lib/plume/ast/unique_table_constraint.rb +20 -0
- data/lib/plume/ast/update_statement.rb +35 -0
- data/lib/plume/ast/upsert_clause.rb +26 -0
- data/lib/plume/ast/vacuum_statement.rb +18 -0
- data/lib/plume/ast/values_clause.rb +12 -0
- data/lib/plume/ast/values_row.rb +13 -0
- data/lib/plume/ast/variable.rb +15 -0
- data/lib/plume/ast/window_definition.rb +26 -0
- data/lib/plume/ast/with_clause.rb +15 -0
- data/lib/plume/ast/without_rowid_table_option.rb +6 -0
- data/lib/plume/catalog.rb +276 -0
- data/lib/plume/comment.rb +73 -0
- data/lib/plume/dump.rb +143 -0
- data/lib/plume/fingerprint.rb +86 -0
- data/lib/plume/lexer.rb +640 -0
- data/lib/plume/literal_patches.rb +76 -0
- data/lib/plume/normalize.rb +65 -0
- data/lib/plume/parameters.rb +52 -0
- data/lib/plume/parser.rb +4064 -0
- data/lib/plume/pretty_printer.rb +202 -0
- data/lib/plume/printer.rb +525 -0
- data/lib/plume/sqlite_grammar.rb +86 -0
- data/lib/plume/truncate.rb +67 -0
- data/lib/plume/union.rb +28 -0
- data/lib/plume/version.rb +1 -1
- data/lib/plume.rb +274 -3
- 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,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,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,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
|