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,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
# Constants turned into `?` parameters, so the same query with different values comes out the
|
|
5
|
+
# same - for grouping a log the way pg_stat_statements does, or for keeping values out of it.
|
|
6
|
+
# Numbers, strings and blobs are replaced, a sign in front of a number with them. NULL, TRUE,
|
|
7
|
+
# FALSE and CURRENT_* are not, since `IS ?` would not mean `IS NULL`. Neither is an integer
|
|
8
|
+
# standing alone in ORDER BY or GROUP BY, which SQLite takes for a column's position.
|
|
9
|
+
# Only SELECT, INSERT, UPDATE and DELETE (and EXPLAIN of them) are normalized, other statements
|
|
10
|
+
# are left as they were: their constants are schema, not data
|
|
11
|
+
module Normalize
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
STATEMENTS = [SelectStatement, InsertStatement, UpdateStatement, DeleteStatement].freeze
|
|
15
|
+
|
|
16
|
+
# The SQL with each constant swapped for `?` in place, everything else - spacing, case,
|
|
17
|
+
# comments - kept as written
|
|
18
|
+
def text(sql)
|
|
19
|
+
src = sql.b
|
|
20
|
+
spans = Parser.new(sql).parse.flat_map { |statement| constants(statement).map(&:location) }
|
|
21
|
+
out = +""
|
|
22
|
+
at = 0
|
|
23
|
+
spans.sort_by(&:first).each do |beg, fin|
|
|
24
|
+
out << src.byteslice(at, beg - at) << "?"
|
|
25
|
+
at = fin
|
|
26
|
+
end
|
|
27
|
+
out << src.byteslice(at, src.bytesize - at)
|
|
28
|
+
out.force_encoding(sql.encoding)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# A copy of the tree with each constant replaced by a `?` Variable
|
|
32
|
+
def tree(node)
|
|
33
|
+
targets = Set.new(constants(node)).compare_by_identity
|
|
34
|
+
node.transform { |n| targets.include?(n) ? Variable.new(name: "?") : n }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The nodes to replace: literals, or a sign with the literal under it
|
|
38
|
+
def constants(statement)
|
|
39
|
+
root = (ExplainStatement === statement) ? statement.statement : statement
|
|
40
|
+
return [] unless STATEMENTS.any? { |klass| klass === root }
|
|
41
|
+
|
|
42
|
+
found = []
|
|
43
|
+
# By identity: hashing a node hashes its whole subtree
|
|
44
|
+
positional = Set.new.compare_by_identity
|
|
45
|
+
root.walk_with_slots do |node, parent, prop|
|
|
46
|
+
case node
|
|
47
|
+
when OrderingTerm
|
|
48
|
+
positional << node if SelectStatement === parent && prop == :orderings
|
|
49
|
+
when UnaryExpression
|
|
50
|
+
found << node if (node.operator in :NEGATE | :IDENTITY) && value?(node.operand)
|
|
51
|
+
when LiteralExpression
|
|
52
|
+
next if UnaryExpression === parent && (parent.operator in :NEGATE | :IDENTITY)
|
|
53
|
+
next if Integer === node.value && (positional.include?(parent) || (SelectCore === parent && prop == :groupings))
|
|
54
|
+
|
|
55
|
+
found << node if value?(node)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
found
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def value?(node) = LiteralExpression === node && (node.value in Numeric | String)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
# The bind parameters of one statement, numbered the way SQLite numbers them: `?` takes the
|
|
5
|
+
# next number, `?N` takes N, and a name keeps the number it first got. `count` and `names`
|
|
6
|
+
# are what `sqlite3_bind_parameter_count` and `sqlite3_bind_parameter_name` would answer
|
|
7
|
+
class Parameters
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
10
|
+
# One place a parameter appears. `name` is nil for a bare `?`
|
|
11
|
+
Parameter = Data.define(:number, :name, :node)
|
|
12
|
+
|
|
13
|
+
attr_reader :count, :names
|
|
14
|
+
|
|
15
|
+
def initialize(statement)
|
|
16
|
+
slots = {}
|
|
17
|
+
@count = 0
|
|
18
|
+
@parameters = variables(statement).map do |variable|
|
|
19
|
+
name = variable.name
|
|
20
|
+
number =
|
|
21
|
+
if name == "?"
|
|
22
|
+
@count += 1
|
|
23
|
+
elsif (n = name[/\A\?(\d+)\z/, 1]&.to_i)
|
|
24
|
+
slots[n] ||= name
|
|
25
|
+
@count = n if n > @count
|
|
26
|
+
n
|
|
27
|
+
elsif (n = slots.key(name))
|
|
28
|
+
n
|
|
29
|
+
else
|
|
30
|
+
slots[@count += 1] = name
|
|
31
|
+
@count
|
|
32
|
+
end
|
|
33
|
+
Parameter.new(number:, name: (name == "?") ? nil : name, node: variable)
|
|
34
|
+
end.freeze
|
|
35
|
+
@names = Array.new(@count) { |i| slots[i + 1] }.freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def each(&) = @parameters.each(&)
|
|
39
|
+
|
|
40
|
+
def size = @parameters.size
|
|
41
|
+
|
|
42
|
+
# The number a name was given, as `sqlite3_bind_parameter_index` answers it
|
|
43
|
+
def index(name) = @names.index(name)&.+(1)
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# SQLite numbers them as it reads them, which is not always the order of the tree
|
|
48
|
+
def variables(statement)
|
|
49
|
+
statement.each_node.grep(Variable).each_with_index.sort_by { |v, i| [v.location&.first || 0, i] }.map(&:first)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|