Skip to content

Commit 17928e9

Browse files
authored
Merge pull request #347 from ruby-syntax-tree/database
Experimental database API
2 parents 491b86d + ed215c4 commit 17928e9

File tree

2 files changed

+332
-0
lines changed

2 files changed

+332
-0
lines changed

lib/syntax_tree.rb

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module SyntaxTree
2121
# CLI. Requiring those features takes time, so we autoload as many constants
2222
# as possible in order to keep the CLI as fast as possible.
2323

24+
autoload :Database, "syntax_tree/database"
2425
autoload :DSL, "syntax_tree/dsl"
2526
autoload :FieldVisitor, "syntax_tree/field_visitor"
2627
autoload :Index, "syntax_tree/index"

lib/syntax_tree/database.rb

+331
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
# frozen_string_literal: true
2+
3+
module SyntaxTree
4+
# Provides the ability to index source files into a database, then query for
5+
# the nodes.
6+
module Database
7+
class IndexingVisitor < SyntaxTree::FieldVisitor
8+
attr_reader :database, :filepath, :node_id
9+
10+
def initialize(database, filepath)
11+
@database = database
12+
@filepath = filepath
13+
@node_id = nil
14+
end
15+
16+
private
17+
18+
def comments(node)
19+
end
20+
21+
def field(name, value)
22+
return unless value.is_a?(SyntaxTree::Node)
23+
24+
binds = [node_id, visit(value), name]
25+
database.execute(<<~SQL, binds)
26+
INSERT INTO edges (from_id, to_id, name)
27+
VALUES (?, ?, ?)
28+
SQL
29+
end
30+
31+
def list(name, values)
32+
values.each_with_index do |value, index|
33+
binds = [node_id, visit(value), name, index]
34+
database.execute(<<~SQL, binds)
35+
INSERT INTO edges (from_id, to_id, name, list_index)
36+
VALUES (?, ?, ?, ?)
37+
SQL
38+
end
39+
end
40+
41+
def node(node, _name)
42+
previous = node_id
43+
binds = [
44+
node.class.name.delete_prefix("SyntaxTree::"),
45+
filepath,
46+
node.location.start_line,
47+
node.location.start_column
48+
]
49+
50+
database.execute(<<~SQL, binds)
51+
INSERT INTO nodes (type, path, line, column)
52+
VALUES (?, ?, ?, ?)
53+
SQL
54+
55+
begin
56+
@node_id = database.last_insert_row_id
57+
yield
58+
@node_id
59+
ensure
60+
@node_id = previous
61+
end
62+
end
63+
64+
def text(name, value)
65+
end
66+
67+
def pairs(name, values)
68+
values.each_with_index do |(key, value), index|
69+
binds = [node_id, visit(key), "#{name}[0]", index]
70+
database.execute(<<~SQL, binds)
71+
INSERT INTO edges (from_id, to_id, name, list_index)
72+
VALUES (?, ?, ?, ?)
73+
SQL
74+
75+
binds = [node_id, visit(value), "#{name}[1]", index]
76+
database.execute(<<~SQL, binds)
77+
INSERT INTO edges (from_id, to_id, name, list_index)
78+
VALUES (?, ?, ?, ?)
79+
SQL
80+
end
81+
end
82+
end
83+
84+
# Query for a specific type of node.
85+
class TypeQuery
86+
attr_reader :type
87+
88+
def initialize(type)
89+
@type = type
90+
end
91+
92+
def each(database, &block)
93+
sql = "SELECT * FROM nodes WHERE type = ?"
94+
database.execute(sql, type).each(&block)
95+
end
96+
end
97+
98+
# Query for the attributes of a node, optionally also filtering by type.
99+
class AttrQuery
100+
attr_reader :type, :attrs
101+
102+
def initialize(type, attrs)
103+
@type = type
104+
@attrs = attrs
105+
end
106+
107+
def each(database, &block)
108+
joins = []
109+
binds = []
110+
111+
attrs.each do |name, query|
112+
ids = query.each(database).map { |row| row[0] }
113+
joins << <<~SQL
114+
JOIN edges AS #{name}
115+
ON #{name}.from_id = nodes.id
116+
AND #{name}.name = ?
117+
AND #{name}.to_id IN (#{(["?"] * ids.size).join(", ")})
118+
SQL
119+
120+
binds.push(name).concat(ids)
121+
end
122+
123+
sql = +"SELECT nodes.* FROM nodes, edges #{joins.join(" ")}"
124+
125+
if type
126+
sql << " WHERE nodes.type = ?"
127+
binds << type
128+
end
129+
130+
sql << " GROUP BY nodes.id"
131+
database.execute(sql, binds).each(&block)
132+
end
133+
end
134+
135+
# Query for the results of either query.
136+
class OrQuery
137+
attr_reader :left, :right
138+
139+
def initialize(left, right)
140+
@left = left
141+
@right = right
142+
end
143+
144+
def each(database, &block)
145+
left.each(database, &block)
146+
right.each(database, &block)
147+
end
148+
end
149+
150+
# A lazy query result.
151+
class QueryResult
152+
attr_reader :database, :query
153+
154+
def initialize(database, query)
155+
@database = database
156+
@query = query
157+
end
158+
159+
def each(&block)
160+
return enum_for(__method__) unless block_given?
161+
query.each(database, &block)
162+
end
163+
end
164+
165+
# A pattern matching expression that will be compiled into a query.
166+
class Pattern
167+
class CompilationError < StandardError
168+
end
169+
170+
attr_reader :query
171+
172+
def initialize(query)
173+
@query = query
174+
end
175+
176+
def compile
177+
program =
178+
begin
179+
SyntaxTree.parse("case nil\nin #{query}\nend")
180+
rescue Parser::ParseError
181+
raise CompilationError, query
182+
end
183+
184+
compile_node(program.statements.body.first.consequent.pattern)
185+
end
186+
187+
private
188+
189+
def compile_error(node)
190+
raise CompilationError, PP.pp(node, +"").chomp
191+
end
192+
193+
# Shortcut for combining two queries into one that returns the results of
194+
# if either query matches.
195+
def combine_or(left, right)
196+
OrQuery.new(left, right)
197+
end
198+
199+
# in foo | bar
200+
def compile_binary(node)
201+
compile_error(node) if node.operator != :|
202+
203+
combine_or(compile_node(node.left), compile_node(node.right))
204+
end
205+
206+
# in Ident
207+
def compile_const(node)
208+
value = node.value
209+
210+
if SyntaxTree.const_defined?(value, false)
211+
clazz = SyntaxTree.const_get(value)
212+
TypeQuery.new(clazz.name.delete_prefix("SyntaxTree::"))
213+
else
214+
compile_error(node)
215+
end
216+
end
217+
218+
# in SyntaxTree::Ident
219+
def compile_const_path_ref(node)
220+
parent = node.parent
221+
if !parent.is_a?(SyntaxTree::VarRef) ||
222+
!parent.value.is_a?(SyntaxTree::Const)
223+
compile_error(node)
224+
end
225+
226+
if parent.value.value == "SyntaxTree"
227+
compile_node(node.constant)
228+
else
229+
compile_error(node)
230+
end
231+
end
232+
233+
# in Ident[value: String]
234+
def compile_hshptn(node)
235+
compile_error(node) unless node.keyword_rest.nil?
236+
237+
attrs = {}
238+
node.keywords.each do |keyword, value|
239+
compile_error(node) unless keyword.is_a?(SyntaxTree::Label)
240+
attrs[keyword.value.chomp(":")] = compile_node(value)
241+
end
242+
243+
type = node.constant ? compile_node(node.constant).type : nil
244+
AttrQuery.new(type, attrs)
245+
end
246+
247+
# in Foo
248+
def compile_var_ref(node)
249+
value = node.value
250+
251+
if value.is_a?(SyntaxTree::Const)
252+
compile_node(value)
253+
else
254+
compile_error(node)
255+
end
256+
end
257+
258+
def compile_node(node)
259+
case node
260+
when SyntaxTree::Binary
261+
compile_binary(node)
262+
when SyntaxTree::Const
263+
compile_const(node)
264+
when SyntaxTree::ConstPathRef
265+
compile_const_path_ref(node)
266+
when SyntaxTree::HshPtn
267+
compile_hshptn(node)
268+
when SyntaxTree::VarRef
269+
compile_var_ref(node)
270+
else
271+
compile_error(node)
272+
end
273+
end
274+
end
275+
276+
class Connection
277+
attr_reader :raw_connection
278+
279+
def initialize(raw_connection)
280+
@raw_connection = raw_connection
281+
end
282+
283+
def execute(query, binds = [])
284+
raw_connection.execute(query, binds)
285+
end
286+
287+
def index_file(filepath)
288+
program = SyntaxTree.parse(SyntaxTree.read(filepath))
289+
program.accept(IndexingVisitor.new(self, filepath))
290+
end
291+
292+
def last_insert_row_id
293+
raw_connection.last_insert_row_id
294+
end
295+
296+
def prepare
297+
raw_connection.execute(<<~SQL)
298+
CREATE TABLE nodes (
299+
id integer primary key,
300+
type varchar(20),
301+
path varchar(200),
302+
line integer,
303+
column integer
304+
);
305+
SQL
306+
307+
raw_connection.execute(<<~SQL)
308+
CREATE INDEX nodes_type ON nodes (type);
309+
SQL
310+
311+
raw_connection.execute(<<~SQL)
312+
CREATE TABLE edges (
313+
id integer primary key,
314+
from_id integer,
315+
to_id integer,
316+
name varchar(20),
317+
list_index integer
318+
);
319+
SQL
320+
321+
raw_connection.execute(<<~SQL)
322+
CREATE INDEX edges_name ON edges (name);
323+
SQL
324+
end
325+
326+
def search(query)
327+
QueryResult.new(self, Pattern.new(query).compile)
328+
end
329+
end
330+
end
331+
end

0 commit comments

Comments
 (0)