Skip to content

Commit a8232b3

Browse files
committed
feat: type checkerrrrr create
1 parent 4df116f commit a8232b3

File tree

10 files changed

+133
-6
lines changed

10 files changed

+133
-6
lines changed

.github/workflows/ci.yml

+6
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ jobs:
2727
ports:
2828
- 5432:5432
2929

30+
env:
31+
DB_CONNECTION_STRING: postgresql://postgres:postgres@localhost:5432/postgres
32+
3033
steps:
3134
- name: 🏗 Setup repository
3235
uses: actions/checkout@v3
@@ -38,6 +41,9 @@ jobs:
3841
with:
3942
github-token: ${{ secrets.GITHUB_TOKEN }}
4043

44+
- name: Run test migrations
45+
run: psql -f test-db/seed.sql postgresql://postgres:postgres@localhost:5432/postgres
46+
4147
- name: 📦 Build
4248
id: build
4349
run: cargo build

Cargo.lock

+6-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ rust-version = "1.71"
1111
[workspace.dependencies]
1212
ide = { path = "./crates/ide", version = "0.0.0" }
1313
diagnostics = { path = "./crates/diagnostics", version = "0.0.0" }
14-
typecheck = { path = "./crates/typecheck", version = "0.0.0" }
14+
typecheck_sqlx = { path = "./crates/typecheck_sqlx", version = "0.0.0" }
1515
lint = { path = "./crates/lint", version = "0.0.0" }
1616
hover = { path = "./crates/hover", version = "0.0.0" }
1717
base_db = { path = "./crates/base_db", version = "0.0.0" }

crates/ide/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ dashmap = "5.5.3"
99
base_db.workspace = true
1010
lint.workspace = true
1111
hover.workspace = true
12+
sqlx = { version = "0.7.3", features = [ "runtime-async-std", "tls-rustls", "postgres", "json" ] }
1213
sql_parser.workspace = true
1314
diagnostics.workspace = true
1415
tree-sitter.workspace = true

crates/ide/src/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use dashmap::{DashMap, DashSet};
99
use lint::Linter;
1010
use pg_query::PgQueryParser;
1111
use schema_cache::SchemaCache;
12+
use sqlx::PgPool;
1213
use tracing::{event, span, Level};
1314
use tree_sitter::TreeSitterParser;
1415

@@ -107,7 +108,7 @@ impl IDE {
107108
}
108109

109110
/// Drain changed statements to kick off analysis
110-
pub fn compute(&self) {
111+
pub fn compute(&self, conn: Option<PgPool>) {
111112
let changed: Vec<StatementRef> = self
112113
.changed_stmts
113114
.iter()

crates/lsp/src/server.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -290,11 +290,13 @@ impl Server {
290290

291291
let ide = Arc::clone(&self.ide);
292292
let tx = self.internal_tx.clone();
293+
let conn = self.db_conn.as_ref().map(|p| p.pool.clone());
294+
293295
self.pool.execute(move || {
294296
// TODO this should happen on change too but once at a time after some debounced delay
295297
// also on open
296298
// check chatgpt for sample for debouncer
297-
ide.compute();
299+
ide.compute(conn);
298300
tx.send(InternalMessage::PublishDiagnostics(cloned_uri));
299301
});
300302

crates/typecheck/src/lib.rs

-1
This file was deleted.

crates/typecheck/Cargo.toml crates/typecheck_sqlx/Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
[package]
2-
name = "typecheck"
2+
name = "typecheck_sqlx"
33
version = "0.0.0"
44
edition = "2021"
55

66
[dependencies]
77
base_db.workspace = true
88
schema_cache.workspace = true
9+
text-size = "1.1.1"
10+
sql_parser.workspace = true
11+
async-std = "1.12.0"
912
tracing.workspace = true
13+
sqlx = { version = "0.7.3", features = [ "runtime-async-std", "tls-rustls", "postgres", "json" ] }
1014

1115
[dev-dependencies]
1216

crates/typecheck_sqlx/src/lib.rs

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use sqlx::postgres::PgDatabaseError;
2+
use sqlx::postgres::PgSeverity;
3+
use sqlx::Executor;
4+
use sqlx::PgPool;
5+
use text_size::TextRange;
6+
use text_size::TextSize;
7+
8+
pub struct TypecheckerParams<'a> {
9+
pub conn: &'a PgPool,
10+
pub sql: &'a str,
11+
pub enriched_ast: Option<&'a sql_parser::EnrichedAst>,
12+
}
13+
14+
#[derive(Debug, Clone)]
15+
pub struct TypeError {
16+
pub message: String,
17+
pub code: String,
18+
pub severity: PgSeverity,
19+
pub position: Option<usize>,
20+
pub range: Option<TextRange>,
21+
pub table: Option<String>,
22+
pub column: Option<String>,
23+
pub data_type: Option<String>,
24+
pub constraint: Option<String>,
25+
}
26+
27+
pub async fn check_sql<'a>(params: TypecheckerParams<'a>) -> Vec<TypeError> {
28+
let mut errs = vec![];
29+
30+
let res = params.conn.prepare(params.sql).await;
31+
32+
if res.is_err() {
33+
if let sqlx::Error::Database(err) = res.as_ref().unwrap_err() {
34+
let pg_err = err.downcast_ref::<PgDatabaseError>();
35+
36+
let position = match pg_err.position() {
37+
Some(sqlx::postgres::PgErrorPosition::Original(pos)) => Some(pos - 1),
38+
_ => None,
39+
};
40+
41+
let range = match params.enriched_ast {
42+
Some(ast) => {
43+
if position.is_none() {
44+
None
45+
} else {
46+
ast.covering_node(TextRange::empty(
47+
TextSize::try_from(position.unwrap()).unwrap(),
48+
))
49+
.map(|node| node.range())
50+
}
51+
}
52+
None => None,
53+
};
54+
55+
errs.push(TypeError {
56+
message: pg_err.message().to_string(),
57+
code: pg_err.code().to_string(),
58+
severity: pg_err.severity(),
59+
position,
60+
range,
61+
table: pg_err.table().map(|s| s.to_string()),
62+
column: pg_err.column().map(|s| s.to_string()),
63+
data_type: pg_err.data_type().map(|s| s.to_string()),
64+
constraint: pg_err.constraint().map(|s| s.to_string()),
65+
});
66+
}
67+
}
68+
69+
errs
70+
}
71+
72+
#[cfg(test)]
73+
mod tests {
74+
use async_std::task::block_on;
75+
use sql_parser::parse_ast;
76+
use sqlx::PgPool;
77+
78+
use crate::{check_sql, TypecheckerParams};
79+
80+
#[test]
81+
fn test_check_sql() {
82+
let input = "select id, unknown from contact;";
83+
84+
let conn_string = std::env::var("DB_CONNECTION_STRING").unwrap();
85+
86+
let pool = block_on(PgPool::connect(conn_string.as_str())).unwrap();
87+
88+
let root = sql_parser::parse_sql_statement(input).unwrap();
89+
let ast = parse_ast(input, &root).ast;
90+
91+
let errs = block_on(check_sql(TypecheckerParams {
92+
conn: &pool,
93+
sql: input,
94+
enriched_ast: Some(&ast),
95+
}));
96+
97+
assert_eq!(errs.len(), 1);
98+
99+
let e = &errs[0];
100+
101+
assert_eq!(&input[e.range.unwrap()], "unknown");
102+
}
103+
}

test-db/seed.sql

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
create table public.contact (
2+
id serial primary key not null,
3+
created_at timestamp with time zone not null default now(),
4+
username text
5+
);
6+

0 commit comments

Comments
 (0)