surrealql-language-server 0.6.0

Language Server Protocol implementation for SurrealQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
use std::collections::HashMap;
use std::sync::Arc;

use ls_types::{Diagnostic, DocumentSymbol, Location, Range, SymbolKind, Uri};
use serde::{Deserialize, Serialize};
use tree_sitter::Tree;

use crate::semantic::text::LineIndex;
use crate::semantic::type_expr::TypeExpr;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum SymbolOrigin {
    Builtin,
    Inferred,
    Remote,
    Local,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AccessResult {
    Allowed,
    Denied,
    Unknown,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum QueryAction {
    Select,
    Create,
    Update,
    Delete,
    Relate,
    Execute,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InferenceFact {
    pub confidence: f32,
    pub origin: SymbolOrigin,
    pub evidence: String,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PermissionMode {
    Full,
    None,
    Expression(String),
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PermissionRule {
    pub actions: Vec<QueryAction>,
    pub mode: PermissionMode,
    pub raw: String,
    pub origin: SymbolOrigin,
    pub location: Option<Location>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FieldDef {
    pub table: String,
    pub name: String,
    pub type_expr: Option<TypeExpr>,
    pub comment: Option<String>,
    pub permissions: Vec<PermissionRule>,
    pub origin: SymbolOrigin,
    pub explicit: bool,
    pub inference: Option<InferenceFact>,
    pub location: Location,
}

/// The `TYPE RELATION IN a|b OUT c|d` half of a `DEFINE TABLE`.
///
/// A faithful record of what the source declares, which is why it lives on
/// [`TableDef`] rather than on the merged model. The *derived* graph — which
/// includes edges only a `RELATE` statement witnesses — is
/// [`MergedSemanticModel::graph_edges`] instead.
///
/// Either list can be empty: `TYPE RELATION` alone is legal and constrains
/// neither endpoint.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RelationDef {
    /// The tables an edge row may point *from*, written `IN` or `FROM`.
    pub in_tables: Vec<String>,
    /// The tables an edge row may point *to*, written `OUT` or `TO`.
    pub out_tables: Vec<String>,
    pub enforced: bool,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TableDef {
    pub name: String,
    pub schema_mode: Option<String>,
    pub comment: Option<String>,
    pub permissions: Vec<PermissionRule>,
    pub origin: SymbolOrigin,
    pub explicit: bool,
    pub inference: Option<InferenceFact>,
    pub location: Location,
    /// `Some` only for a table declared `TYPE RELATION`. `#[serde(default)]`
    /// keeps a previously-serialized definition loading.
    #[serde(default)]
    pub relation: Option<RelationDef>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EventDef {
    pub table: String,
    pub name: String,
    pub comment: Option<String>,
    pub when_clause: Option<String>,
    pub then_clause: Option<String>,
    pub origin: SymbolOrigin,
    pub location: Location,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IndexDef {
    pub table: String,
    pub name: String,
    pub fields: Vec<String>,
    pub unique: bool,
    pub options: Vec<String>,
    pub origin: SymbolOrigin,
    pub location: Location,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum FunctionLanguage {
    #[default]
    SurrealQL,
    JavaScript,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FunctionParam {
    pub name: String,
    pub type_expr: Option<TypeExpr>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FunctionDef {
    pub name: String,
    pub params: Vec<FunctionParam>,
    pub return_type: Option<TypeExpr>,
    pub language: FunctionLanguage,
    pub comment: Option<String>,
    pub permissions: Vec<PermissionRule>,
    pub origin: SymbolOrigin,
    pub explicit: bool,
    pub inference: Option<InferenceFact>,
    pub location: Location,
    pub selection_range: Range,
    pub body_range: Option<Range>,
    pub called_functions: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ParamDef {
    pub name: String,
    pub value_preview: Option<String>,
    pub comment: Option<String>,
    pub origin: SymbolOrigin,
    pub location: Location,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AccessDef {
    pub name: String,
    pub comment: Option<String>,
    pub origin: SymbolOrigin,
    pub location: Location,
}

/// A `DEFINE ANALYZER`.
///
/// Indexed because an analyzer name is referenced by name elsewhere —
/// `DEFINE INDEX … FULLTEXT ANALYZER <name>` — and completion cannot offer a
/// name that nothing extracts.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AnalyzerDef {
    pub name: String,
    pub comment: Option<String>,
    pub origin: SymbolOrigin,
    pub location: Location,
}

/// One `RELATE a->edge->b` sighting: proof that `edge` joins two tables.
///
/// Most SurrealQL schemas never declare their edge tables — SurrealDB's own
/// graph corpus defines `person` as `SCHEMALESS` and creates `knows` purely
/// through `RELATE`. Without this, the graph would be empty for exactly the
/// projects that use graphs most.
///
/// An observation is weaker evidence than a `TYPE RELATION` declaration, so
/// [`MergedSemanticModel::reindex_graph_edges`] reads the declarations first.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EdgeObservation {
    /// The edge table — the middle subject of the `RELATE`.
    pub edge: String,
    /// The table the edge points from. `None` when the subject is a
    /// `$parameter`, a call, or an array, which name no table statically.
    pub from: Option<String>,
    /// The table the edge points to, under the same rule as [`Self::from`].
    pub to: Option<String>,
}

/// A name paired with the tight range of the token that produced it,
/// so diagnostics can underline `prson` instead of the whole statement.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NamedRange {
    pub name: String,
    pub range: Range,
}

/// Why a statement's target table list is (or isn't) statically known.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum TargetResolution {
    /// Targets are literal table names / record ids.
    Static,
    /// The target is a `$parameter` — resolvable only at runtime, and
    /// not worth a "could not be resolved" warning.
    Parameter,
    /// The target is an expression (function call, subquery, block).
    Expression,
    /// Nothing recognisable — the legacy "dynamic" case.
    #[default]
    Unresolved,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QueryFact {
    pub action: QueryAction,
    pub target_tables: Vec<String>,
    pub touched_fields: Vec<String>,
    pub dynamic: bool,
    pub location: Location,
    /// Tight token ranges for [`Self::target_tables`] entries.
    /// `#[serde(default)]` keeps previously-serialized facts loading.
    #[serde(default)]
    pub target_refs: Vec<NamedRange>,
    /// Tight token ranges for [`Self::touched_fields`] entries.
    #[serde(default)]
    pub field_refs: Vec<NamedRange>,
    /// How the target list was resolved (drives warning suppression
    /// for `$param` / expression targets).
    #[serde(default)]
    pub target_resolution: TargetResolution,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SymbolReference {
    pub name: String,
    pub kind: SymbolKind,
    pub location: Location,
    pub selection_range: Range,
}

#[derive(Debug, Clone)]
pub struct DocumentAnalysis {
    pub uri: Uri,
    pub text: String,
    /// The tree-sitter parse of [`Self::text`], cached so request
    /// handlers (semantic tokens, inlay hints, …) reuse it instead of
    /// re-parsing the document on every call. `Tree::clone` is a shallow,
    /// ref-counted copy, so storing it is cheap. This is also the
    /// foundation for incremental re-parsing once the server moves to
    /// incremental document sync.
    pub tree: Tree,
    /// Line start offsets for [`Self::text`], built once per analysis so every
    /// byte-offset-to-[`Position`] conversion is a binary search rather than a
    /// scan from byte 0. Request handlers reuse it for cursor lookups too.
    ///
    /// [`Position`]: ls_types::Position
    pub line_index: LineIndex,
    pub tables: Vec<TableDef>,
    pub events: Vec<EventDef>,
    pub indexes: Vec<IndexDef>,
    pub fields: Vec<FieldDef>,
    pub functions: Vec<FunctionDef>,
    pub params: Vec<ParamDef>,
    pub accesses: Vec<AccessDef>,
    pub analyzers: Vec<AnalyzerDef>,
    pub query_facts: Vec<QueryFact>,
    /// Every `RELATE a->edge->b` this document writes, in source order.
    pub edge_observations: Vec<EdgeObservation>,
    pub references: Vec<SymbolReference>,
    pub syntax_diagnostics: Vec<Diagnostic>,
    pub document_symbols: Vec<DocumentSymbol>,
}

/// What a workspace scan had to skip. Non-zero counters are reported
/// to the client so silent truncation doesn't look like coverage.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct WorkspaceScanStats {
    /// Directory entries the walker could not read (permissions, IO).
    pub walk_errors: usize,
    /// Files skipped because they exceed the size ceiling.
    pub skipped_oversize: usize,
    /// Files that matched but could not be read as UTF-8 text.
    pub skipped_unreadable: usize,
    /// True when the workspace file cap stopped the scan early.
    pub file_cap_hit: bool,
}

/// Documents are shared via [`Arc`] so that cloning a [`WorkspaceIndex`]
/// across the background task / read-snapshot boundary is O(documents) pointer
/// copies instead of O(total source bytes) string clones.
#[derive(Debug, Clone, Default)]
pub struct WorkspaceIndex {
    pub documents: HashMap<Uri, Arc<DocumentAnalysis>>,
    pub scan_stats: WorkspaceScanStats,
}

#[derive(Debug, Clone, Default)]
pub struct LiveMetadataSnapshot {
    pub documents: HashMap<Uri, Arc<DocumentAnalysis>>,
    pub errors: Vec<String>,
}

/// Which way a graph hop points.
///
/// The grammar has three arrow kinds and this mirrors them: `->` is
/// [`Self::Right`], `<-` and `<~` are [`Self::Left`], and `<->` is
/// [`Self::Both`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LookupDirection {
    Right,
    Left,
    Both,
}

impl LookupDirection {
    /// Pick which of a rightward and a leftward index this direction reads.
    ///
    /// [`Self::Both`] reads each in turn, so every caller stays a single loop
    /// instead of branching three ways. Yields references, and allocates
    /// nothing — this runs on the completion path.
    pub fn maps<'a, T>(self, rightward: &'a T, leftward: &'a T) -> impl Iterator<Item = &'a T> {
        let (first, second) = match self {
            Self::Right => (rightward, None),
            Self::Left => (leftward, None),
            Self::Both => (rightward, Some(leftward)),
        };
        std::iter::once(first).chain(second)
    }
}

/// Which edge tables leave, and which arrive at, each table.
///
/// Both maps are keyed by an *endpoint* table name and hold edge table names,
/// which is the direction completion and type inference ask in: "I am on
/// `person` and I typed `->` — what can I traverse?"
///
/// `outgoing["person"]` answers for `->`; `incoming["person"]` answers for
/// `<-`. An edge that declares neither endpoint appears in neither map, and an
/// edge whose two endpoints are the same table appears in both.
#[derive(Debug, Clone, Default)]
pub struct GraphIndex {
    /// Endpoint table → edge tables reachable with `->`.
    pub outgoing: HashMap<String, Vec<String>>,
    /// Endpoint table → edge tables reachable with `<-`.
    pub incoming: HashMap<String, Vec<String>>,
    /// Edge table → the tables it points *to*, for the second hop of
    /// `->edge->target`.
    pub edge_targets: HashMap<String, Vec<String>>,
    /// Edge table → the tables it points *from*, for `<-edge<-source`.
    pub edge_sources: HashMap<String, Vec<String>>,
}

#[derive(Debug, Clone, Default)]
pub struct MergedSemanticModel {
    pub tables: HashMap<String, TableDef>,
    pub events: HashMap<(String, String), EventDef>,
    pub indexes: HashMap<(String, String), IndexDef>,
    /// Every field, grouped by the table it belongs to: table name → field
    /// name → definition.
    ///
    /// Nested rather than keyed by a `(table, field)` tuple so a lookup
    /// borrows both halves instead of allocating them. A tuple key cannot be
    /// borrowed from a `(&str, &str)` pair, so every read of a flat map had to
    /// build — and then drop — two `String`s. `fields_for_table` paid that per
    /// field, on a path completion runs per table.
    ///
    /// The outer map also replaces the separate `fields_by_table` index: the
    /// inner map's keys *are* the field names of a table.
    ///
    /// Insert through [`MergedSemanticModel::insert_field`][insert_field],
    /// which applies the origin-priority merge.
    ///
    /// [insert_field]: MergedSemanticModel::insert_field
    pub fields: HashMap<String, HashMap<String, FieldDef>>,
    pub functions: HashMap<String, FunctionDef>,
    pub params: HashMap<String, ParamDef>,
    pub accesses: HashMap<String, AccessDef>,
    pub analyzers: HashMap<String, AnalyzerDef>,
    pub function_references: HashMap<String, Vec<Location>>,
    pub function_callers: HashMap<String, Vec<String>>,
    /// The return type read out of a function *body*, for the functions that
    /// declare none. Keyed by full name, `fn::` prefix included.
    ///
    /// A derived cross-document fact, so it belongs here rather than on
    /// [`FunctionDef`] — the same reason [`Self::function_callers`] does.
    /// `FunctionDef` stays a faithful record of what the source says, and
    /// `FunctionDef::return_type` keeps meaning "the author wrote this".
    ///
    /// Filled by
    /// [`crate::semantic::infer::infer_function_return_types`].
    pub inferred_function_returns: HashMap<String, TypeExpr>,
    pub workspace_symbols: Vec<DocumentSymbol>,
    pub query_facts: HashMap<Uri, Vec<QueryFact>>,
    /// How many query facts across the workspace target each table name.
    ///
    /// Derived from [`Self::query_facts`] by
    /// [`MergedSemanticModel::reindex_target_usage`][reindex]. The
    /// unknown-table check needs this per inferred target in the document being
    /// diagnosed, and counting it on demand meant flattening every fact in the
    /// workspace each time.
    ///
    /// [reindex]: MergedSemanticModel::reindex_target_usage
    pub target_usage: HashMap<String, usize>,
    /// The names of the *explicitly defined* tables — the only candidates a
    /// "did you mean" sweep may offer.
    ///
    /// Derived, and maintained by
    /// [`MergedSemanticModel::insert_table`][insert_table] alongside
    /// [`Self::tables`]. Insert through that method rather than writing to
    /// `tables` directly, or the sweep will not see the table.
    ///
    /// Exists because the sweep read `tables.values()` and filtered on
    /// `explicit` afterwards. In a workspace where most tables are inferred from
    /// usage that walks the whole map — thousands of ~230-byte entries streamed
    /// to read one `bool` — to reach a candidate set a fraction of the size.
    ///
    /// [insert_table]: MergedSemanticModel::insert_table
    pub explicit_tables: Vec<String>,
    /// True when the live-metadata fetch reported errors while this
    /// model was built — remote tables may be missing, so
    /// unknown-name judgments are unreliable until recovery.
    pub metadata_degraded: bool,
    /// The graph topology, derived from every `TableDef::relation` and every
    /// [`EdgeObservation`] in the workspace.
    ///
    /// Derived and cross-document, so it belongs here rather than on
    /// [`TableDef`] — the same reason [`Self::function_callers`] does, and a
    /// load-bearing one: [`MergedSemanticModel::insert_table`] *replaces* a
    /// `TableDef` wholesale when a higher-origin definition wins, which would
    /// discard any observed edges stored there.
    ///
    /// Rebuilt by
    /// [`MergedSemanticModel::reindex_graph_edges`][reindex_graph_edges].
    ///
    /// [reindex_graph_edges]: MergedSemanticModel::reindex_graph_edges
    pub graph_edges: GraphIndex,
}