diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..057163a94 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,155 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Postgres Language Server implementation providing LSP features for SQL development, including autocompletion, syntax error highlighting, type-checking, and linting. The project is built in Rust using a modular crate architecture and includes TypeScript packages for editor integrations. + +## Key Commands + +### Development Setup +```bash +# Install development tools +just install-tools + +# Start database for schema introspection +docker-compose up -d + +# For Nix users +nix develop && docker-compose up -d +``` + +### Building and Testing +```bash +# Run all tests +just test +# or: cargo test run --no-fail-fast + +# Test specific crate +just test-crate pgt_lsp + +# Run doc tests +just test-doc +``` + +### Code Quality +```bash +# Format all code (Rust, TOML, JS/TS) +just format + +# Lint entire codebase +just lint +# or: cargo clippy && cargo run -p rules_check && bun biome lint + +# Fix linting issues +just lint-fix + +# Full ready check (run before committing) +just ready +``` + +### Code Generation +```bash +# Generate linter code and configuration +just gen-lint + +# Create new lint rule +just new-lintrule [severity] + +# Create new crate +just new-crate +``` + +### CLI Usage +The main CLI binary is `postgrestools`: +```bash +cargo run -p pgt_cli -- check file.sql +# or after building: +./target/release/postgrestools check file.sql +``` + +## Architecture + +### Crate Structure +The project uses a modular Rust workspace with crates prefixed with `pgt_`: + +**Core Infrastructure:** +- `pgt_workspace` - Main API and workspace management +- `pgt_lsp` - Language Server Protocol implementation +- `pgt_cli` - Command-line interface +- `pgt_fs` - Virtual file system abstraction +- `pgt_configuration` - Configuration management + +**Parser and Language Processing:** +- `pgt_query` - Postgres query parsing (wraps libpg_query) +- `pgt_lexer` - SQL tokenizer with whitespace handling +- `pgt_statement_splitter` - Splits source into individual statements +- `pgt_treesitter` - Tree-sitter integration for additional parsing + +**Features:** +- `pgt_completions` - Autocompletion engine +- `pgt_hover` - Hover information provider +- `pgt_analyser` & `pgt_analyse` - Linting and analysis framework +- `pgt_typecheck` - Type checking via EXPLAIN +- `pgt_schema_cache` - In-memory database schema representation + +**Utilities:** +- `pgt_diagnostics` - Error and warning reporting +- `pgt_console` - Terminal output and formatting +- `pgt_text_edit` - Text manipulation utilities +- `pgt_suppressions` - Rule suppression handling + +### TypeScript Packages +Located in `packages/` and `editors/`: +- VSCode extension in `editors/code/` +- Backend JSON-RPC bridge in `packages/@postgrestools/backend-jsonrpc/` +- Main TypeScript package in `packages/@postgrestools/postgrestools/` + +### Database Integration +The server connects to a Postgres database to build an in-memory schema cache containing tables, columns, functions, and type information. This enables accurate autocompletion and type checking. + +### Statement Processing Flow +1. Input source code is split into individual SQL statements +2. Each statement is parsed using libpg_query (via `pgt_query`) +3. Statements are analyzed against the schema cache +4. Results are cached and updated incrementally on file changes + +## Testing + +### Test Data Location +- SQL test cases: `crates/pgt_statement_splitter/tests/data/` +- Analyzer test specs: `crates/pgt_analyser/tests/specs/` +- Example SQL files: `example/`, `test.sql` + +### Snapshot Testing +The project uses `insta` for snapshot testing. Update snapshots with: +```bash +cargo insta review +``` + +## Configuration Files + +### Rust Configuration +- `Cargo.toml` - Workspace definition with all crate dependencies +- `rust-toolchain.toml` - Rust version specification +- `rustfmt.toml` - Code formatting configuration +- `clippy.toml` - Clippy linting configuration + +### Other Tools +- `biome.jsonc` - Biome formatter/linter configuration for JS/TS +- `taplo.toml` - TOML formatting configuration +- `justfile` - Task runner with all development commands +- `docker-compose.yml` - Database setup for testing + +## Development Notes + +### Code Generation +Many parser structures are generated from PostgreSQL's protobuf definitions using procedural macros in `pgt_query_macros`. Run `just gen-lint` after modifying analyzer rules or configurations. + +### Database Schema +The `pgt_schema_cache` crate contains SQL queries in `src/queries/` that introspect the database schema to build the in-memory cache. + +### Multi-Platform Support +The project includes platform-specific allocators and build configurations for Windows, macOS, and Linux. +- Seeing the Treesitter tree for an SQL query can be helpful to debug and implement features. To do this, create a file with an SQL query, and run `just tree-print `. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 057163a94..ec1e25056 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,155 +1,4 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +@./AGENTS.md -## Project Overview - -This is a Postgres Language Server implementation providing LSP features for SQL development, including autocompletion, syntax error highlighting, type-checking, and linting. The project is built in Rust using a modular crate architecture and includes TypeScript packages for editor integrations. - -## Key Commands - -### Development Setup -```bash -# Install development tools -just install-tools - -# Start database for schema introspection -docker-compose up -d - -# For Nix users -nix develop && docker-compose up -d -``` - -### Building and Testing -```bash -# Run all tests -just test -# or: cargo test run --no-fail-fast - -# Test specific crate -just test-crate pgt_lsp - -# Run doc tests -just test-doc -``` - -### Code Quality -```bash -# Format all code (Rust, TOML, JS/TS) -just format - -# Lint entire codebase -just lint -# or: cargo clippy && cargo run -p rules_check && bun biome lint - -# Fix linting issues -just lint-fix - -# Full ready check (run before committing) -just ready -``` - -### Code Generation -```bash -# Generate linter code and configuration -just gen-lint - -# Create new lint rule -just new-lintrule [severity] - -# Create new crate -just new-crate -``` - -### CLI Usage -The main CLI binary is `postgrestools`: -```bash -cargo run -p pgt_cli -- check file.sql -# or after building: -./target/release/postgrestools check file.sql -``` - -## Architecture - -### Crate Structure -The project uses a modular Rust workspace with crates prefixed with `pgt_`: - -**Core Infrastructure:** -- `pgt_workspace` - Main API and workspace management -- `pgt_lsp` - Language Server Protocol implementation -- `pgt_cli` - Command-line interface -- `pgt_fs` - Virtual file system abstraction -- `pgt_configuration` - Configuration management - -**Parser and Language Processing:** -- `pgt_query` - Postgres query parsing (wraps libpg_query) -- `pgt_lexer` - SQL tokenizer with whitespace handling -- `pgt_statement_splitter` - Splits source into individual statements -- `pgt_treesitter` - Tree-sitter integration for additional parsing - -**Features:** -- `pgt_completions` - Autocompletion engine -- `pgt_hover` - Hover information provider -- `pgt_analyser` & `pgt_analyse` - Linting and analysis framework -- `pgt_typecheck` - Type checking via EXPLAIN -- `pgt_schema_cache` - In-memory database schema representation - -**Utilities:** -- `pgt_diagnostics` - Error and warning reporting -- `pgt_console` - Terminal output and formatting -- `pgt_text_edit` - Text manipulation utilities -- `pgt_suppressions` - Rule suppression handling - -### TypeScript Packages -Located in `packages/` and `editors/`: -- VSCode extension in `editors/code/` -- Backend JSON-RPC bridge in `packages/@postgrestools/backend-jsonrpc/` -- Main TypeScript package in `packages/@postgrestools/postgrestools/` - -### Database Integration -The server connects to a Postgres database to build an in-memory schema cache containing tables, columns, functions, and type information. This enables accurate autocompletion and type checking. - -### Statement Processing Flow -1. Input source code is split into individual SQL statements -2. Each statement is parsed using libpg_query (via `pgt_query`) -3. Statements are analyzed against the schema cache -4. Results are cached and updated incrementally on file changes - -## Testing - -### Test Data Location -- SQL test cases: `crates/pgt_statement_splitter/tests/data/` -- Analyzer test specs: `crates/pgt_analyser/tests/specs/` -- Example SQL files: `example/`, `test.sql` - -### Snapshot Testing -The project uses `insta` for snapshot testing. Update snapshots with: -```bash -cargo insta review -``` - -## Configuration Files - -### Rust Configuration -- `Cargo.toml` - Workspace definition with all crate dependencies -- `rust-toolchain.toml` - Rust version specification -- `rustfmt.toml` - Code formatting configuration -- `clippy.toml` - Clippy linting configuration - -### Other Tools -- `biome.jsonc` - Biome formatter/linter configuration for JS/TS -- `taplo.toml` - TOML formatting configuration -- `justfile` - Task runner with all development commands -- `docker-compose.yml` - Database setup for testing - -## Development Notes - -### Code Generation -Many parser structures are generated from PostgreSQL's protobuf definitions using procedural macros in `pgt_query_macros`. Run `just gen-lint` after modifying analyzer rules or configurations. - -### Database Schema -The `pgt_schema_cache` crate contains SQL queries in `src/queries/` that introspect the database schema to build the in-memory cache. - -### Multi-Platform Support -The project includes platform-specific allocators and build configurations for Windows, macOS, and Linux. -- Seeing the Treesitter tree for an SQL query can be helpful to debug and implement features. To do this, create a file with an SQL query, and run `just tree-print `. \ No newline at end of file diff --git a/crates/pgt_configuration/src/database.rs b/crates/pgt_configuration/src/database.rs index 39efb8d13..9ca2ffa0b 100644 --- a/crates/pgt_configuration/src/database.rs +++ b/crates/pgt_configuration/src/database.rs @@ -9,6 +9,11 @@ use serde::{Deserialize, Serialize}; #[partial(cfg_attr(feature = "schema", derive(schemars::JsonSchema)))] #[partial(serde(rename_all = "camelCase", default, deny_unknown_fields))] pub struct DatabaseConfiguration { + /// A connection string that encodes the full connection setup. + /// When provided, it takes precedence over the individual fields. + #[partial(bpaf(long("connection-string")))] + pub connection_string: Option, + /// The host of the database. /// Required if you want database-related features. /// All else falls back to sensible defaults. @@ -47,6 +52,7 @@ pub struct DatabaseConfiguration { impl Default for DatabaseConfiguration { fn default() -> Self { Self { + connection_string: None, disable_connection: false, host: "127.0.0.1".to_string(), port: 5432, diff --git a/crates/pgt_configuration/src/lib.rs b/crates/pgt_configuration/src/lib.rs index b1242b082..0ad51c5a8 100644 --- a/crates/pgt_configuration/src/lib.rs +++ b/crates/pgt_configuration/src/lib.rs @@ -131,6 +131,7 @@ impl PartialConfiguration { ..Default::default() }), db: Some(PartialDatabaseConfiguration { + connection_string: None, host: Some("127.0.0.1".to_string()), port: Some(5432), username: Some("postgres".to_string()), diff --git a/crates/pgt_workspace/src/settings.rs b/crates/pgt_workspace/src/settings.rs index 9b5efbd9b..ede73c76a 100644 --- a/crates/pgt_workspace/src/settings.rs +++ b/crates/pgt_workspace/src/settings.rs @@ -5,6 +5,7 @@ use std::{ borrow::Cow, num::NonZeroU64, path::{Path, PathBuf}, + str::FromStr, sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}, time::Duration, }; @@ -20,6 +21,7 @@ use pgt_configuration::{ plpgsql_check::PlPgSqlCheckConfiguration, }; use pgt_fs::PgTPath; +use sqlx::postgres::PgConnectOptions; use crate::{ WorkspaceError, @@ -467,6 +469,7 @@ impl Default for TypecheckSettings { #[derive(Debug)] pub struct DatabaseSettings { pub enable_connection: bool, + pub connection_string: Option, pub host: String, pub port: u16, pub username: String, @@ -480,6 +483,7 @@ impl Default for DatabaseSettings { fn default() -> Self { Self { enable_connection: false, + connection_string: None, host: "127.0.0.1".to_string(), port: 5432, username: "postgres".to_string(), @@ -495,15 +499,38 @@ impl From for DatabaseSettings { fn from(value: PartialDatabaseConfiguration) -> Self { let d = DatabaseSettings::default(); - // "host" is the minimum required setting for database features - // to be enabled. - let enable_connection = value - .host - .as_ref() - .is_some_and(|_| value.disable_connection.is_none_or(|disabled| !disabled)); + let connection_string = value.connection_string.and_then(|uri| { + let trimmed = uri.trim().to_string(); + if trimmed.is_empty() { + None + } else { + Some(trimmed) + } + }); - let database = value.database.unwrap_or(d.database); - let host = value.host.unwrap_or(d.host); + // "host" OR "connectionString" is the minimum required setting for database features + // to be enabled. + let disable_connection = value.disable_connection.is_some_and(|disabled| disabled); + let enable_connection = + (connection_string.is_some() || value.host.as_ref().is_some()) && !disable_connection; + + let mut database = value.database.unwrap_or(d.database); + let mut host = value.host.unwrap_or(d.host); + let mut port = value.port.unwrap_or(d.port); + let mut username = value.username.unwrap_or(d.username); + + if let Some(uri) = connection_string.as_ref() { + let opts = PgConnectOptions::from_str(uri) + .unwrap_or_else(|err| panic!("Invalid db.connectionString provided: {err}")); + + // we only extract the values we need for statement execution checks and connection key + host = opts.get_host().to_string(); + port = opts.get_port(); + username = opts.get_username().to_string(); + if let Some(db) = opts.get_database() { + database = db.to_string(); + } + } let allow_statement_executions = value .allow_statement_executions_against @@ -520,9 +547,10 @@ impl From for DatabaseSettings { Self { enable_connection, + connection_string, - port: value.port.unwrap_or(d.port), - username: value.username.unwrap_or(d.username), + port, + username, password: value.password.unwrap_or(d.password), database, host, @@ -633,4 +661,39 @@ mod tests { assert!(!config.allow_statement_executions) } + + #[test] + fn connection_string_enables_statement_executions_matching_host() { + let partial_config = PartialDatabaseConfiguration { + connection_string: Some( + "postgres://postgres:postgres@localhost:5432/test-db".to_string(), + ), + allow_statement_executions_against: Some(StringSet::from_iter(vec![String::from( + "localhost/*", + )])), + ..Default::default() + }; + + let config = DatabaseSettings::from(partial_config); + + assert!(config.enable_connection); + assert!(config.allow_statement_executions) + } + + #[test] + fn connection_string_respects_statement_execution_filters() { + let partial_config = PartialDatabaseConfiguration { + connection_string: Some( + "postgres://postgres:postgres@prod-host:5432/prod-db".to_string(), + ), + allow_statement_executions_against: Some(StringSet::from_iter(vec![String::from( + "localhost/*", + )])), + ..Default::default() + }; + + let config = DatabaseSettings::from(partial_config); + + assert!(!config.allow_statement_executions) + } } diff --git a/crates/pgt_workspace/src/workspace/server.rs b/crates/pgt_workspace/src/workspace/server.rs index e5cebbe9a..3fb9eaeb5 100644 --- a/crates/pgt_workspace/src/workspace/server.rs +++ b/crates/pgt_workspace/src/workspace/server.rs @@ -408,13 +408,11 @@ impl Workspace for WorkspaceServer { }); }; - let pool = self.get_current_connection(); - if pool.is_none() { + let Some(pool) = self.get_current_connection() else { return Ok(ExecuteStatementResult { message: "No database connection available.".into(), }); - } - let pool = pool.unwrap(); + }; let result = run_async(async move { pool.execute(sqlx::query(&content)).await })??; @@ -699,14 +697,12 @@ impl Workspace for WorkspaceServer { .get(¶ms.path) .ok_or(WorkspaceError::not_found())?; - let pool = self.get_current_connection(); - if pool.is_none() { + let Some(pool) = self.get_current_connection() else { tracing::debug!("No database connection available. Skipping completions."); return Ok(CompletionsResult::default()); - } - let pool = pool.unwrap(); + }; - let schema_cache = self.schema_cache.load(pool)?; + let schema_cache = self.schema_cache.load(pool.clone())?; match get_statement_for_completions(parsed_doc, params.position) { None => { @@ -739,14 +735,12 @@ impl Workspace for WorkspaceServer { .get(¶ms.path) .ok_or(WorkspaceError::not_found())?; - let pool = self.get_current_connection(); - if pool.is_none() { + let Some(pool) = self.get_current_connection() else { tracing::debug!("No database connection available. Skipping completions."); return Ok(OnHoverResult::default()); - } - let pool = pool.unwrap(); + }; - let schema_cache = self.schema_cache.load(pool)?; + let schema_cache = self.schema_cache.load(pool.clone())?; match doc .iter_with_filter( diff --git a/crates/pgt_workspace/src/workspace/server/connection_manager.rs b/crates/pgt_workspace/src/workspace/server/connection_manager.rs index 145b6fa0c..c6251c82c 100644 --- a/crates/pgt_workspace/src/workspace/server/connection_manager.rs +++ b/crates/pgt_workspace/src/workspace/server/connection_manager.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::str::FromStr; use std::sync::RwLock; use std::time::{Duration, Instant}; @@ -71,12 +72,22 @@ impl ConnectionManager { }); // Create a new pool - let config = PgConnectOptions::new() - .host(&settings.host) - .port(settings.port) - .username(&settings.username) - .password(&settings.password) - .database(&settings.database); + let config = if let Some(uri) = settings.connection_string.as_ref() { + match PgConnectOptions::from_str(uri) { + Ok(options) => options, + Err(err) => { + tracing::error!("Failed to parse database connection URI: {err}"); + return None; + } + } + } else { + PgConnectOptions::new() + .host(&settings.host) + .port(settings.port) + .username(&settings.username) + .password(&settings.password) + .database(&settings.database) + }; let timeout = settings.conn_timeout_secs; diff --git a/crates/pgt_workspace/src/workspace/server/schema_cache_manager.rs b/crates/pgt_workspace/src/workspace/server/schema_cache_manager.rs index 007ebb782..2a7b77957 100644 --- a/crates/pgt_workspace/src/workspace/server/schema_cache_manager.rs +++ b/crates/pgt_workspace/src/workspace/server/schema_cache_manager.rs @@ -22,7 +22,6 @@ impl SchemaCacheManager { pub fn load(&self, pool: PgPool) -> Result, WorkspaceError> { let key: ConnectionKey = (&pool).into(); - // Try read lock first for cache hit if let Ok(schemas) = self.schemas.read() { if let Some(cache) = schemas.get(&key) { diff --git a/docs/configuration.md b/docs/configuration.md index 5bdf231e7..70fe4b84a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -45,6 +45,19 @@ Some tools that the Postgres Language Server provides are implemented as mere in } ``` +When you need to pass additional Postgres settings (e.g. `sslmode`, `options`, +`application_name`) you can provide a connection string instead of the +individual fields. The URI takes precedence over any other connection fields. + +```json +{ + "db": { + "connectionString": "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable", + "allowStatementExecutionsAgainst": ["localhost/*"] + } +} +``` + ## Specifying files to process @@ -92,5 +105,3 @@ In the following example, we include all files, except those in any test/ folder #### Control files via VCS You can ignore files ignored by your [VCS](/guides/vcs_integration.md). - - diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 5e6f64f27..565efd420 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -118,6 +118,8 @@ Runs everything to the requested files. The directory where the migration files are stored - **` --after`**=_`ARG`_ — Ignore any migrations before this timestamp +- **` --connection-string`**=_`ARG`_ — + Provide a libpq-compatible connection string. When set, it overrides the individual connection flags below. - **` --host`**=_`ARG`_ — The host of the database. - **` --port`**=_`ARG`_ — diff --git a/docs/schema.json b/docs/schema.json index 55f1abbfb..93f8a4331 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -125,6 +125,13 @@ "format": "uint16", "minimum": 0.0 }, + "connectionString": { + "description": "A connection string that encodes the full connection setup. When provided, it takes precedence over the individual fields.", + "type": [ + "string", + "null" + ] + }, "database": { "description": "The name of the database.", "type": [