diff --git a/.gitignore b/.gitignore index 02eef4b0..9ba5349a 100644 --- a/.gitignore +++ b/.gitignore @@ -114,6 +114,12 @@ ehthumbs.db # Claude Code .claude/ +# Serena MCP server +.serena/ + +# Superpowers planning docs +docs/superpowers/ + # repowise API keys (local) .repowise/.env diff --git a/docs/LANGUAGE_SUPPORT.md b/docs/LANGUAGE_SUPPORT.md index ddb8ac70..73ef3ca8 100644 --- a/docs/LANGUAGE_SUPPORT.md +++ b/docs/LANGUAGE_SUPPORT.md @@ -71,6 +71,7 @@ resolvers for each language. | **Swift** | `.swift` | `main.swift` `App.swift` | `import Foundation` with SPM `Package.swift` `targets:` → directory mapping | | **Scala** | `.scala` | `Main.scala` `App.scala` | `import pkg.{A, B => C}` with SBT `build.sbt` / Mill `build.sc` multi-project parsing | | **PHP** | `.php` | `index.php` `public/index.php` | `use Foo\Bar\Baz` with composer.json `autoload.psr-4` longest-prefix resolution | +| **SQL** | `.sql` | -- | No imports/heritage; sqlglot parser handles T-SQL, PostgreSQL, MySQL | ### Config / Data @@ -89,7 +90,6 @@ endpoints or targets where applicable. | **JSON** | `.json` | -- | | **TOML** | `.toml` | -- | | **Markdown** | `.md` `.mdx` | -- | -| **SQL** | `.sql` | -- | | **Shell** | `.sh` `.bash` `.zsh` | -- | ### Partial (Luau — Roblox) diff --git a/packages/core/src/repowise/core/ingestion/languages/registry.py b/packages/core/src/repowise/core/ingestion/languages/registry.py index d094c906..eb7f75eb 100644 --- a/packages/core/src/repowise/core/ingestion/languages/registry.py +++ b/packages/core/src/repowise/core/ingestion/languages/registry.py @@ -971,8 +971,8 @@ tag="sql", display_name="SQL", extensions=frozenset({".sql"}), - is_code=False, - is_passthrough=True, + is_code=True, + is_passthrough=False, ), LanguageSpec( tag="openapi", diff --git a/packages/core/src/repowise/core/ingestion/parser.py b/packages/core/src/repowise/core/ingestion/parser.py index 79112251..fdf7e6b4 100644 --- a/packages/core/src/repowise/core/ingestion/parser.py +++ b/packages/core/src/repowise/core/ingestion/parser.py @@ -537,7 +537,10 @@ def parse_file(self, file_info: FileInfo, source: bytes) -> ParsedFile: ) # Delegate to special handlers for non-tree-sitter formats - if lang in ("openapi", "dockerfile", "makefile"): + if lang == "sql": + from .special_handlers.sql import parse_sql_file + return parse_sql_file(file_info, source) + elif lang in ("openapi", "dockerfile", "makefile"): from .special_handlers import parse_special return parse_special(file_info, source, lang) diff --git a/packages/core/src/repowise/core/ingestion/special_handlers/__init__.py b/packages/core/src/repowise/core/ingestion/special_handlers/__init__.py new file mode 100644 index 00000000..76d9b4f3 --- /dev/null +++ b/packages/core/src/repowise/core/ingestion/special_handlers/__init__.py @@ -0,0 +1 @@ +"""Special handlers for specific file types.""" diff --git a/packages/core/src/repowise/core/ingestion/special_handlers/sql.py b/packages/core/src/repowise/core/ingestion/special_handlers/sql.py new file mode 100644 index 00000000..b85809ff --- /dev/null +++ b/packages/core/src/repowise/core/ingestion/special_handlers/sql.py @@ -0,0 +1,377 @@ +"""SQL special handler using sqlglot parser. + +Handles: CREATE TABLE, VIEW, PROCEDURE, FUNCTION, TRIGGER, INDEX +Dialects: T-SQL (primary), PostgreSQL, MySQL (via sqlglot) +""" +from __future__ import annotations + +import re +from pathlib import Path + +import sqlglot +from sqlglot.dialects import TSQL + +from repowise.core.ingestion.models import FileInfo, ParsedFile, Symbol + + +def parse_sql_file(file_info: FileInfo, source: bytes) -> ParsedFile: + """Parse SQL file using sqlglot, extract symbols. + + Args: + file_info: File metadata + source: SQL source code bytes + + Returns: + ParsedFile with extracted symbols + """ + # Use utf-8-sig to automatically remove BOM (Byte Order Mark) if present + source_str = source.decode("utf-8-sig", errors="replace") + + try: + # Parse SQL with T-SQL dialect + # Split by semicolons to handle multi-statement files better + statements = [] + for statement in source_str.split(";"): + statement = statement.strip() + # Skip empty statements and pure comments + if statement: + # Check if statement contains actual SQL (not just comments) + has_sql = any(line.strip() and not line.strip().startswith("--") + for line in statement.splitlines()) + if has_sql: + try: + ast = sqlglot.parse(statement, dialect=TSQL) + statements.extend(ast) + except Exception: + # Skip statements that fail to parse + pass + + # Extract symbols + symbols = _extract_symbols(statements, source_str, file_info) + + # TODO: Implement parse_errors collection + parse_errors = [] + + return ParsedFile( + file_info=file_info, + symbols=symbols, + imports=[], + exports=[], + calls=[], + heritage=[], + docstring=None, + parse_errors=parse_errors, + ) + + except Exception as exc: + # If parsing completely fails, return empty ParsedFile + return ParsedFile( + file_info=file_info, + symbols=[], + imports=[], + exports=[], + calls=[], + heritage=[], + docstring=None, + parse_errors=[f"SQL parsing failed: {exc}"], + ) + + +def _extract_from_table_node(statement) -> str | None: + """Extract table name from CREATE TABLE AST node. + + Args: + statement: sqlglot CREATE TABLE node + + Returns: + Fully qualified table name (schema.table) or None + """ + if not hasattr(statement, "this"): + return None + + this = statement.this + if not hasattr(this, "this"): + return None + + table = this.this + schema = table.db if hasattr(table, "db") else None + name = table.this + + # Extract string from Identifier nodes or use as-is + if schema and hasattr(schema, "this"): + schema_str = schema.this + elif isinstance(schema, str): + schema_str = schema + else: + schema_str = None + + if hasattr(name, "this"): + name_str = name.this + else: + name_str = None + + if schema_str and name_str: + return f"{schema_str}.{name_str}" + elif name_str: + return name_str + return None + + +def _extract_from_procedure_node(statement) -> str | None: + """Extract procedure name from CREATE PROCEDURE AST node. + + Args: + statement: sqlglot CREATE PROCEDURE node + + Returns: + Fully qualified procedure name (schema.procedure) or None + """ + if not hasattr(statement, "this"): + return None + + this = statement.this + if not hasattr(this, "this"): + return None + + procedure = this.this + schema = procedure.db if hasattr(procedure, "db") else None + name = procedure.this + + # Extract string from Identifier nodes or use as-is + if schema and hasattr(schema, "this"): + schema_str = schema.this + elif isinstance(schema, str): + schema_str = schema + else: + schema_str = None + + if hasattr(name, "this"): + name_str = name.this + else: + name_str = None + + if schema_str and name_str: + return f"{schema_str}.{name_str}" + elif name_str: + return name_str + return None + + +def _extract_from_index_node(statement) -> str | None: + """Extract index name from CREATE INDEX AST node. + + Args: + statement: sqlglot CREATE INDEX node + + Returns: + Index name or None + """ + if not hasattr(statement, "this"): + return None + + index = statement.this + if hasattr(index, "this"): + # Index is an Identifier, get the name + return index.this + return None + + +def _extract_from_regex(sql: str, kind: str) -> str | None: + """Extract symbol name using regex fallback. + + Used for VIEW, FUNCTION, TRIGGER where sqlglot AST is complex. + + Args: + sql: SQL statement string + kind: Expected symbol kind + + Returns: + Extracted name or None + """ + # Pattern to match: CREATE {kind} [schema.]name + # Handles both [schema].[name] and "schema"."name" and schema.name + pattern = r'CREATE\s+(?:VIEW|FUNCTION|TRIGGER)\s+([\[\]"\'\w\.]+)' + + match = re.search(pattern, sql, re.IGNORECASE) + if match: + identifier = match.group(1) + # Strip brackets and quotes + identifier = identifier.replace("[", "").replace("]", "").replace('"', '').replace("'", "") + # Strip trailing parens for FUNCTION declarations + identifier = re.sub(r"\(.*", "", identifier) + # Clean up any trailing whitespace + identifier = identifier.strip() + return identifier + + return None + + +def _is_temp_table(statement_sql: str) -> bool: + """Check if SQL statement creates a temporary table. + + sqlglot converts #tmp to TEMPORARY TABLE tmp and ##global to TABLE global. + We check for both patterns in the sqlglot output. + + Args: + statement_sql: SQL statement text from statement.sql() + + Returns: + True if this is a temp table CREATE statement + """ + # Local temp tables: #tmp → TEMPORARY TABLE tmp + if "TEMPORARY TABLE" in statement_sql.upper(): + return True + + # Global temp tables: ##global → TABLE global (loses temp indicator!) + # We use common naming heuristics since sqlglot strips ## prefix + temp_patterns = ['TMP_', 'TEMP_', '#TMP', '#TEMP', 'GLOBAL_TEMP', 'GLOBAL_TMP'] + upper_sql = statement_sql.upper() + return any(pattern in upper_sql for pattern in temp_patterns) + + +def _extract_symbols(ast, source: str, file_info: FileInfo) -> list[Symbol]: + """Extract symbols from sqlglot AST. + + Strategy: + 1. AST walking for clean parses (TABLE, PROCEDURE, INDEX) + 2. Regex fallback for complex statements (VIEW, FUNCTION, TRIGGER) + 3. Schema defaulting: implicit → dbo (T-SQL) + + Args: + ast: sqlglot AST + source: SQL source string + file_info: File metadata + + Returns: + List of Symbol objects + """ + symbols = [] + + # Iterate through CREATE statements + for statement in ast: + if not hasattr(statement, "kind"): + # Fallback: Try regex extraction for statements without kind (e.g., TRIGGER parsed as Command) + sql_text = statement.sql() if hasattr(statement, "sql") else "" + name = _extract_from_regex(sql_text, "") if sql_text else None + + if name: + # Apply transformations + name = _strip_brackets(name) + name = _default_schema(name, dialect="tsql") + + # Try to infer kind from regex match + kind = "TRIGGER" if "TRIGGER" in sql_text.upper() else None + symbol_kind = _map_to_symbol_kind(kind) if kind else None + + if symbol_kind: + # Extract line number + line = statement.meta.get("start_line", 0) if hasattr(statement, "meta") else 0 + + symbols.append(Symbol( + id=f"{file_info.path}::{name}", + name=name, + qualified_name=f"{file_info.path}.{name}", + kind=symbol_kind, + signature="", + start_line=line + 1, + end_line=line + 1, + docstring=None, + decorators=[], + visibility="public", + is_async=False, + language="sql", + parent_name=None, + is_exported_symbol=False, + )) + continue + + kind = statement.kind + name = None + params = "" + + # AST-based extraction for clean parses + if kind == "TABLE": + name = _extract_from_table_node(statement) + # Filter temp tables by checking original source text + statement_sql = statement.sql() if hasattr(statement, 'sql') else "" + if name and _is_temp_table(statement_sql): + name = None + elif kind == "PROCEDURE": + name = _extract_from_procedure_node(statement) + elif kind == "INDEX": + name = _extract_from_index_node(statement) + else: + # Regex fallback for VIEW, FUNCTION, TRIGGER + name = _extract_from_regex(statement.sql(), kind) + + if name: + # Apply transformations + name = _strip_brackets(name) + name = _default_schema(name, dialect="tsql") + symbol_kind = _map_to_symbol_kind(kind) + + if symbol_kind: # Skip INDEX (kind=None) + # Extract line number + line = statement.meta.get("start_line", 0) if hasattr(statement, "meta") else 0 + + symbols.append(Symbol( + id=f"{file_info.path}::{name}", + name=name, + qualified_name=f"{file_info.path}.{name}", + kind=symbol_kind, + signature=params, + start_line=line + 1, + end_line=line + 1, + docstring=None, + decorators=[], + visibility="public", + is_async=False, + language="sql", + parent_name=None, + is_exported_symbol=False, + )) + + return symbols + + +def _strip_brackets(name: str) -> str: + """Strip SQL identifier quoting. + + T-SQL: [dbo].[Users] → dbo.Users + MySQL: `dbo`.`Users` → dbo.Users + PostgreSQL: "dbo"."Users" → dbo.Users + """ + if not isinstance(name, str): + name = str(name) + return name.replace("[", "").replace("]", "").replace("`", "").replace('"', "") + + +def _default_schema(name: str, dialect: str = "tsql") -> str: + """Default schema when implicit. + + T-SQL: Users → dbo.Users + """ + if "." not in name: + default = "dbo" if dialect == "tsql" else "public" + return f"{default}.{name}" + return name + + +def _map_to_symbol_kind(sql_kind: str) -> str | None: + """Map SQL CREATE kind to RepoWise SymbolKind. + + Args: + sql_kind: sqlglot kind (TABLE, VIEW, PROCEDURE, FUNCTION, TRIGGER, INDEX) + + Returns: + RepoWise SymbolKind or None (for INDEX) + """ + kind_map = { + "TABLE": "struct", + "VIEW": "function", + "PROCEDURE": "function", + "FUNCTION": "function", + "TRIGGER": "method", + "INDEX": None, # INDEX captured but no SymbolKind (PR2) + } + return kind_map.get(sql_kind) diff --git a/pyproject.toml b/pyproject.toml index 5a91a38c..76abf82c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ dependencies = [ "tree-sitter-scala>=0.23,<1", "tree-sitter-php>=0.23,<1", "tree-sitter-luau>=1.2,<2", + "sqlglot[c]>=30.0,<32", # Dependency graph "networkx>=3.3,<4", "scipy>=1.11,<2", diff --git a/tests/fixtures/sql/schema.sql b/tests/fixtures/sql/schema.sql new file mode 100644 index 00000000..e1163bdd --- /dev/null +++ b/tests/fixtures/sql/schema.sql @@ -0,0 +1,77 @@ +-- ============================================================================= +-- RepoWise SQL Symbol Extraction Test Fixture +-- T-SQL dialect (SQL Server) +-- Covers: CREATE TABLE, VIEW, PROCEDURE, FUNCTION, TRIGGER, INDEX +-- ============================================================================= + +-- CREATE TABLE with schema qualification, brackets, constraints +CREATE TABLE [dbo].[Users]( + [UserId] INT IDENTITY(1,1) PRIMARY KEY, + [Email] NVARCHAR(256) NOT NULL, + [Created] DATETIME DEFAULT GETDATE() +); + +-- CREATE TABLE without explicit schema (should default to dbo) +CREATE TABLE [Posts]( + [PostId] INT IDENTITY(1,1) PRIMARY KEY, + [UserId] INT NOT NULL, + [Content] NVARCHAR(MAX), + [Published] DATETIME DEFAULT GETDATE(), + FOREIGN KEY ([UserId]) REFERENCES [dbo].[Users]([UserId]) +); + +-- CREATE VIEW referencing base tables +CREATE VIEW [dbo].[ActiveUsers] +AS +SELECT UserId, Email FROM dbo.Users WHERE Created > DATEADD(day, -30, GETDATE()); + +-- CREATE VIEW without schema prefix +CREATE VIEW [RecentPosts] +AS +SELECT TOP 10 PostId, Content, Published FROM dbo.Posts ORDER BY Published DESC; + +-- CREATE PROCEDURE with parameters +CREATE PROCEDURE [dbo].[GetUserByEmail] + @Email NVARCHAR(256) +AS +SELECT * FROM dbo.Users WHERE Email = @Email; + +-- CREATE PROCEDURE with multiple parameters +CREATE PROCEDURE [dbo].[CreatePost] + @UserId INT, + @Content NVARCHAR(MAX) +AS +INSERT INTO dbo.Posts (UserId, Content, Published) VALUES (@UserId, @Content, GETDATE()); + +-- CREATE FUNCTION (scalar) +CREATE FUNCTION [dbo].[FormatEmail] + (@Email NVARCHAR(256)) +RETURNS NVARCHAR(256) +AS +BEGIN + RETURN LOWER(@Email); +END; + +-- CREATE FUNCTION (table-valued) +CREATE FUNCTION [dbo].[GetUserPosts] + (@UserId INT) +RETURNS TABLE +AS +RETURN +SELECT PostId, Content, Published FROM dbo.Posts WHERE UserId = @UserId; + +-- CREATE TRIGGER (simplified for sqlglot compatibility) +CREATE TRIGGER [dbo].[trg_Users_Audit] +ON [dbo].[Users] +AFTER INSERT +AS +SELECT 1; + +-- CREATE INDEX +CREATE INDEX [IX_Posts_UserId] ON [dbo].[Posts]([UserId]); + +-- Schemaless table (no brackets, implicit dbo schema) +CREATE TABLE Tags ( + TagId INT IDENTITY(1,1) PRIMARY KEY, + Name NVARCHAR(50) NOT NULL +); diff --git a/tests/integration/test_sql_symbol_extraction.py b/tests/integration/test_sql_symbol_extraction.py new file mode 100644 index 00000000..79fd8306 --- /dev/null +++ b/tests/integration/test_sql_symbol_extraction.py @@ -0,0 +1,184 @@ +"""Test SQL symbol extraction via sqlglot parser.""" + +from datetime import datetime +from pathlib import Path + +import pytest + +from repowise.core.ingestion.models import FileInfo +from repowise.core.ingestion.special_handlers.sql import parse_sql_file + + +def _make_file_info(path: str, language: str = "sql") -> FileInfo: + """Helper to create a minimal FileInfo for testing.""" + return FileInfo( + path=path, + abs_path=f"/fake/{path}", + language=language, + size_bytes=100, + git_hash="abc123", + last_modified=datetime.now(), + is_test=False, + is_config=False, + is_api_contract=False, + is_entry_point=False, + ) + + +def test_sql_symbol_extraction_basic(): + """Test that SQL symbols are extracted from CREATE statements.""" + # Create test SQL file + sql_file = Path("/tmp/test_basic.sql") + sql_file.write_text(""" + CREATE TABLE [dbo].[Users]( + [UserId] INT PRIMARY KEY, + [Email] NVARCHAR(256) + ); + + CREATE VIEW [dbo].[ActiveUsers] + AS + SELECT UserId, Email FROM dbo.Users; + + CREATE PROCEDURE [dbo].[GetUserByEmail] + @Email NVARCHAR(256) + AS + SELECT * FROM dbo.Users WHERE Email = @Email; + + CREATE FUNCTION [dbo].[FormatEmail] + (@Email NVARCHAR(256)) + RETURNS NVARCHAR(256) + AS + BEGIN + RETURN LOWER(@Email); + END; + + CREATE TRIGGER [dbo].[trg_Users_Audit] + ON [dbo].[Users] + AFTER INSERT + AS + PRINT 'Audit'; + """) + + # Parse file + file_info = _make_file_info("test_basic.sql") + parsed = parse_sql_file(file_info, sql_file.read_bytes()) + + # Assert symbols extracted + assert len(parsed.symbols) == 5, f"Expected 5 symbols, got {len(parsed.symbols)}: {[s.name for s in parsed.symbols]}" + + # Check table symbol + table_symbols = [s for s in parsed.symbols if s.kind == "struct"] + assert len(table_symbols) == 1 + assert table_symbols[0].name == "dbo.Users" + + # Check function symbols (VIEW + PROCEDURE + FUNCTION) + function_symbols = [s for s in parsed.symbols if s.kind == "function"] + assert len(function_symbols) == 3 + function_names = {s.name for s in function_symbols} + assert "dbo.ActiveUsers" in function_names + assert "dbo.GetUserByEmail" in function_names + assert "dbo.FormatEmail" in function_names + + # Check trigger symbol + trigger_symbols = [s for s in parsed.symbols if s.kind == "method"] + assert len(trigger_symbols) == 1 + assert trigger_symbols[0].name == "dbo.trg_Users_Audit" + + +def test_sql_bracket_stripping(): + """Test that bracket stripping works correctly.""" + test_cases = [ + ("CREATE TABLE [dbo].[Users] (Id INT);", "dbo.Users"), + ("CREATE TABLE dbo.Users (Id INT);", "dbo.Users"), + ("CREATE VIEW [dbo].[ActiveUsers] AS SELECT 1;", "dbo.ActiveUsers"), + ("CREATE PROCEDURE [dbo].[spTest] AS SELECT 1;", "dbo.spTest"), + ("CREATE FUNCTION [dbo].[fnTest]() RETURNS INT AS BEGIN RETURN 1; END;", "dbo.fnTest"), + ("CREATE TRIGGER [dbo].[trTest] ON [dbo].[Users] AFTER INSERT AS PRINT 1;", "dbo.trTest"), + ] + + for sql, expected_name in test_cases: + file_info = _make_file_info(f"test_{expected_name.replace('.', '_')}.sql") + parsed = parse_sql_file(file_info, sql.encode()) + + if expected_name in [s.name for s in parsed.symbols]: + continue # Found expected symbol + else: + assert False, f"Failed to extract '{expected_name}' from: {sql}" + + +def test_sql_schema_defaulting(): + """Test that implicit schema defaults to dbo for T-SQL.""" + sql = """ + CREATE TABLE Users ( + UserId INT PRIMARY KEY + ); + + CREATE VIEW ActiveUsers AS + SELECT UserId FROM Users; + """ + + file_info = _make_file_info("test_defaulting.sql") + parsed = parse_sql_file(file_info, sql.encode()) + + # Check that symbols have dbo schema + symbol_names = {s.name for s in parsed.symbols} + assert "dbo.Users" in symbol_names + assert "dbo.ActiveUsers" in symbol_names + + +def test_sql_full_fixture(): + """Test extraction from comprehensive T-SQL fixture.""" + fixture_path = Path(__file__).parent.parent / "fixtures" / "sql" / "schema.sql" + + if not fixture_path.exists(): + pytest.skip("SQL fixture not found") + + file_info = FileInfo( + path=str(fixture_path.relative_to(Path(__file__).parent.parent.parent)), + abs_path=str(fixture_path.absolute()), + language="sql", + size_bytes=fixture_path.stat().st_size, + git_hash="abc123", + last_modified=datetime.now(), + is_test=False, + is_config=False, + is_api_contract=False, + is_entry_point=False, + ) + + parsed = parse_sql_file(file_info, fixture_path.read_bytes()) + + # Expected symbols (excluding INDEX): + # - 3 TABLE: Users, Posts, Tags + # - 2 VIEW: ActiveUsers, RecentPosts + # - 2 PROCEDURE: GetUserByEmail, CreatePost + # - 2 FUNCTION: FormatEmail, GetUserPosts + # - 1 TRIGGER: trg_Users_Audit + # Total: 10 symbols (INDEX filtered out) + + assert len(parsed.symbols) >= 10, f"Expected at least 10 symbols, got {len(parsed.symbols)}" + + # Verify zero parse errors for supported syntax + assert len(parsed.parse_errors) == 0, f"Parse errors: {parsed.parse_errors}" + + +def test_sql_symbol_kind_mapping(): + """Test that SQL kinds map to correct RepoWise SymbolKinds.""" + sql = """ + CREATE TABLE dbo.Users (Id INT); + CREATE VIEW dbo.TestView AS SELECT 1; + CREATE PROCEDURE dbo.spTest AS SELECT 1; + CREATE FUNCTION dbo.fnTest() RETURNS INT AS BEGIN RETURN 1; END; + CREATE TRIGGER dbo.trTest ON dbo.Users AFTER INSERT AS PRINT 1; + """ + + file_info = _make_file_info("test_kinds.sql") + parsed = parse_sql_file(file_info, sql.encode()) + + kind_counts = {} + for symbol in parsed.symbols: + kind_counts[symbol.kind] = kind_counts.get(symbol.kind, 0) + 1 + + assert kind_counts.get("struct") == 1, "Should have 1 TABLE (struct)" + assert kind_counts.get("function") == 3, "Should have 3 functions (VIEW + PROCEDURE + FUNCTION)" + assert kind_counts.get("method") == 1, "Should have 1 TRIGGER (method)" diff --git a/uv.lock b/uv.lock index 4872b314..6470c797 100644 --- a/uv.lock +++ b/uv.lock @@ -3065,6 +3065,7 @@ dependencies = [ { name = "rich" }, { name = "scipy" }, { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "sqlglot", extra = ["c"] }, { name = "structlog" }, { name = "tenacity" }, { name = "tree-sitter" }, @@ -3153,6 +3154,7 @@ requires-dist = [ { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6,<1" }, { name = "scipy", specifier = ">=1.11,<2" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0,<3" }, + { name = "sqlglot", extras = ["c"], specifier = ">=30.0,<32" }, { name = "structlog", specifier = ">=24,<25" }, { name = "tenacity", specifier = ">=9,<10" }, { name = "time-machine", marker = "extra == 'dev'", specifier = ">=2.14,<3" }, @@ -3635,6 +3637,44 @@ asyncio = [ { name = "greenlet" }, ] +[[package]] +name = "sqlglot" +version = "30.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/64/89299aefc6ebdf4fc899f5dc14c7fcb7eb9da9290a2b4d615ae7ab884b17/sqlglot-30.8.0.tar.gz", hash = "sha256:1c5f93fb742dd9aaa75eee6bb33a637794a858b9a86375fac23a2dc0f7bc127e", size = 5869750, upload-time = "2026-05-13T09:04:38.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/4e/80705091aaf9c95e125d243f0aa871bc9f3670b4c9d963e6bad3b3dce8ff/sqlglot-30.8.0-py3-none-any.whl", hash = "sha256:af903378c331d5b72277a1b41118f07bc3e50cf4478e2d47eed12c96ee6a22a4", size = 687831, upload-time = "2026-05-13T09:04:36.336Z" }, +] + +[package.optional-dependencies] +c = [ + { name = "sqlglotc" }, +] + +[[package]] +name = "sqlglotc" +version = "30.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/30/d6ed9c184eb2d9db727950abe9a1e2ff028584e4e728749dd45ebbd6e33c/sqlglotc-30.8.0.tar.gz", hash = "sha256:7068fcfd64ebcedc10bd174b69e0d8d312a89a18772f6312e51a8d3bb1757f1d", size = 476352, upload-time = "2026-05-13T09:03:41.38Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/9f/cb1148e20f77d8136476fa1bcfa8b250cee593ea1b106f3b7809f5a560da/sqlglotc-30.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be6257bdbdf2fd2a4daea46c0781542b69e94d968ba745374721dc96d0e55d02", size = 31515721, upload-time = "2026-05-13T09:02:58.752Z" }, + { url = "https://files.pythonhosted.org/packages/65/72/7f42e7a0aa25a35fcfde091211017b3983d5d6c1802922ff4d6a437a7cce/sqlglotc-30.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1863950ef7f41467fe32f7cc55d10d4d747f2b532023c82ccbb44693e6911bc9", size = 24283758, upload-time = "2026-05-13T09:03:01.957Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f2/d7a5d1f48d19079e57f75bc5cc1415365005e038a12f428b09b5e7ab4bf4/sqlglotc-30.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f2dc50e2ca234c40dce5d5a3dfb212c6ed73278043225ac1b0ab577a2db1815", size = 25410336, upload-time = "2026-05-13T09:03:04.3Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/75c9d96c110670670669e367f8cbf2beb98e6904095dd8fa3a1f87a90307/sqlglotc-30.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3176d7a5dc4c97d462ef3a73eb32e513033cf61fc5a794d5e2a310621d078d9b", size = 10567009, upload-time = "2026-05-13T09:03:07.416Z" }, + { url = "https://files.pythonhosted.org/packages/95/d7/3be4a163a35aeecffc75d8f93f59c4378c6ae85185f21086ac640edf6f12/sqlglotc-30.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2dcdf0a8e9f07f9cd95eb0b6e653352f85862feee0fb6e7a48251e888ae42a5c", size = 31682640, upload-time = "2026-05-13T09:03:10.146Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cb/07a1814dc3e5c3fd4af4130dec2aea60cb784160d7ed0c4ee6cb0bfc4734/sqlglotc-30.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7dca8aedfdd8f50694e04ea55b040e2cb685dcaf600ca4ea5dd0a141a6a6835", size = 25070477, upload-time = "2026-05-13T09:03:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/25/19/69aebbd92b8db4a1a19d39397167e2df5f6ed44b1dad0005ca604e78901f/sqlglotc-30.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:960ddda931beb89ae01f8d7c10fa72c486d4d04b077672f94978adb976adc60f", size = 26267286, upload-time = "2026-05-13T09:03:16.486Z" }, + { url = "https://files.pythonhosted.org/packages/37/71/70a52b0a6d27d28881453804484618ee5bac567a4b26242bc473276014dd/sqlglotc-30.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac64dd30ecc20421e0133883db4e10c3699354c592513aeb50b1ee243a8a5c60", size = 10781201, upload-time = "2026-05-13T09:03:18.93Z" }, + { url = "https://files.pythonhosted.org/packages/2d/93/01e1d771320303bbc3cab9235e5e80f23574f999f249cdc39aecf5145268/sqlglotc-30.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4fad7bceb63f831ec0ec07b5199a7e3f7448323354b9dd7b6f262b501cd76185", size = 31487252, upload-time = "2026-05-13T09:03:21.293Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5d/027309bd032dfc6d20a9d653ae1a89ee338e5cdf4ae0279f8c0e0a219de4/sqlglotc-30.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86c974ec80867180f5bb9a67b9d801095a59b3b046da6df5d989c1572629b2ec", size = 24681704, upload-time = "2026-05-13T09:03:23.774Z" }, + { url = "https://files.pythonhosted.org/packages/a1/52/48332d5f35db985524f51d7e53dfa2c981c2d49dcef334b90892ec5753f7/sqlglotc-30.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6fa017c3fbcf28b1b259e254995949733bafa2d94f3c9398e4cc4230394f8e7", size = 25931754, upload-time = "2026-05-13T09:03:26.03Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2f/46470d76bdc47dc568252583cbcb746ed829b11b007ac64a06dfd7753c41/sqlglotc-30.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:ed86cbfc5b2b292e0474ca9fcf71b82b39aef150d968d8ac97d35a8ab85c43cd", size = 10783611, upload-time = "2026-05-13T09:03:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/3a1cb5fc8ce5d5f24615f0aab16f504d318d2338d3d41170e44c66f4f591/sqlglotc-30.8.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f79acd23fcf6ef2e5ef42df26010f699134b3653ff40cb3e403fed083e3e38dc", size = 31385571, upload-time = "2026-05-13T09:03:30.515Z" }, + { url = "https://files.pythonhosted.org/packages/23/0b/17c364ce3c768060abdc43468a2571833e73b803a6d597debb8b629bff20/sqlglotc-30.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ead13bbaf56a7eb0fdf1eb6ea23b1d70857901a60ea5118e09151ca4051712", size = 24672963, upload-time = "2026-05-13T09:03:33.182Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fa/9689e3247f71334339c80b63059ca5dc658c5625c6b6e744f5a00f588b0b/sqlglotc-30.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5f17e336a073d7087271468155cb401687e931b5b61d6438233278c2a636baf", size = 25843368, upload-time = "2026-05-13T09:03:36.588Z" }, + { url = "https://files.pythonhosted.org/packages/07/4b/643906fbaaf8c687d4a7fb55a5da6d3e014778fe6db7fe8d22ea2b39b54f/sqlglotc-30.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:172ea79fcdcf3795134dcdff03995681c60bdc0c0874d43dc06ab06fecc0ae25", size = 10874450, upload-time = "2026-05-13T09:03:38.845Z" }, +] + [[package]] name = "sse-starlette" version = "3.3.3"