sqlsaber 0.11.0__tar.gz → 0.13.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sqlsaber might be problematic. Click here for more details.

Files changed (71) hide show
  1. sqlsaber-0.13.0/.github/workflows/claude-code-review.yml +78 -0
  2. sqlsaber-0.13.0/.github/workflows/claude.yml +64 -0
  3. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/CHANGELOG.md +15 -0
  4. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/PKG-INFO +10 -1
  5. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/README.md +9 -0
  6. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/pyproject.toml +1 -1
  7. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/cli/commands.py +18 -29
  8. sqlsaber-0.13.0/src/sqlsaber/database/resolver.py +96 -0
  9. sqlsaber-0.13.0/tests/test_database_resolver.py +126 -0
  10. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/uv.lock +1 -1
  11. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/.github/workflows/publish.yml +0 -0
  12. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/.gitignore +0 -0
  13. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/.python-version +0 -0
  14. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/AGENT.md +0 -0
  15. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/CLAUDE.md +0 -0
  16. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/LICENSE +0 -0
  17. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/pytest.ini +0 -0
  18. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/sqlsaber.svg +0 -0
  19. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/__init__.py +0 -0
  20. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/__main__.py +0 -0
  21. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/agents/__init__.py +0 -0
  22. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/agents/anthropic.py +0 -0
  23. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/agents/base.py +0 -0
  24. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/agents/mcp.py +0 -0
  25. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/agents/streaming.py +0 -0
  26. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/cli/__init__.py +0 -0
  27. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/cli/auth.py +0 -0
  28. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/cli/completers.py +0 -0
  29. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/cli/database.py +0 -0
  30. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/cli/display.py +0 -0
  31. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/cli/interactive.py +0 -0
  32. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/cli/memory.py +0 -0
  33. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/cli/models.py +0 -0
  34. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/cli/streaming.py +0 -0
  35. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/clients/__init__.py +0 -0
  36. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/clients/anthropic.py +0 -0
  37. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/clients/base.py +0 -0
  38. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/clients/exceptions.py +0 -0
  39. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/clients/models.py +0 -0
  40. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/clients/streaming.py +0 -0
  41. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/config/__init__.py +0 -0
  42. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/config/api_keys.py +0 -0
  43. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/config/auth.py +0 -0
  44. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/config/database.py +0 -0
  45. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/config/oauth_flow.py +0 -0
  46. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/config/oauth_tokens.py +0 -0
  47. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/config/settings.py +0 -0
  48. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/database/__init__.py +0 -0
  49. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/database/connection.py +0 -0
  50. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/database/schema.py +0 -0
  51. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/mcp/__init__.py +0 -0
  52. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/mcp/mcp.py +0 -0
  53. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/memory/__init__.py +0 -0
  54. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/memory/manager.py +0 -0
  55. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/memory/storage.py +0 -0
  56. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/models/__init__.py +0 -0
  57. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/models/events.py +0 -0
  58. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/src/sqlsaber/models/types.py +0 -0
  59. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/tests/__init__.py +0 -0
  60. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/tests/conftest.py +0 -0
  61. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/tests/test_agents/test_anthropic_oauth.py +0 -0
  62. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/tests/test_cli/__init__.py +0 -0
  63. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/tests/test_cli/test_commands.py +0 -0
  64. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/tests/test_clients/test_anthropic_client.py +0 -0
  65. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/tests/test_clients/test_streaming.py +0 -0
  66. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/tests/test_config/__init__.py +0 -0
  67. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/tests/test_config/test_database.py +0 -0
  68. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/tests/test_config/test_oauth.py +0 -0
  69. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/tests/test_config/test_settings.py +0 -0
  70. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/tests/test_database/__init__.py +0 -0
  71. {sqlsaber-0.11.0 → sqlsaber-0.13.0}/tests/test_database/test_connection.py +0 -0
@@ -0,0 +1,78 @@
1
+ name: Claude Code Review
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize]
6
+ # Optional: Only run on specific file changes
7
+ # paths:
8
+ # - "src/**/*.ts"
9
+ # - "src/**/*.tsx"
10
+ # - "src/**/*.js"
11
+ # - "src/**/*.jsx"
12
+
13
+ jobs:
14
+ claude-review:
15
+ # Optional: Filter by PR author
16
+ # if: |
17
+ # github.event.pull_request.user.login == 'external-contributor' ||
18
+ # github.event.pull_request.user.login == 'new-developer' ||
19
+ # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20
+
21
+ runs-on: ubuntu-latest
22
+ permissions:
23
+ contents: read
24
+ pull-requests: read
25
+ issues: read
26
+ id-token: write
27
+
28
+ steps:
29
+ - name: Checkout repository
30
+ uses: actions/checkout@v4
31
+ with:
32
+ fetch-depth: 1
33
+
34
+ - name: Run Claude Code Review
35
+ id: claude-review
36
+ uses: anthropics/claude-code-action@beta
37
+ with:
38
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39
+
40
+ # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
41
+ # model: "claude-opus-4-20250514"
42
+
43
+ # Direct prompt for automated review (no @claude mention needed)
44
+ direct_prompt: |
45
+ Please review this pull request and provide feedback on:
46
+ - Code quality and best practices
47
+ - Potential bugs or issues
48
+ - Performance considerations
49
+ - Security concerns
50
+ - Test coverage
51
+
52
+ Be constructive and helpful in your feedback.
53
+
54
+ # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
55
+ # use_sticky_comment: true
56
+
57
+ # Optional: Customize review based on file types
58
+ # direct_prompt: |
59
+ # Review this PR focusing on:
60
+ # - For TypeScript files: Type safety and proper interface usage
61
+ # - For API endpoints: Security, input validation, and error handling
62
+ # - For React components: Performance, accessibility, and best practices
63
+ # - For tests: Coverage, edge cases, and test quality
64
+
65
+ # Optional: Different prompts for different authors
66
+ # direct_prompt: |
67
+ # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
68
+ # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
69
+ # 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
70
+
71
+ # Optional: Add specific tools for running tests or linting
72
+ # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
73
+
74
+ # Optional: Skip review for certain conditions
75
+ # if: |
76
+ # !contains(github.event.pull_request.title, '[skip-review]') &&
77
+ # !contains(github.event.pull_request.title, '[WIP]')
78
+
@@ -0,0 +1,64 @@
1
+ name: Claude Code
2
+
3
+ on:
4
+ issue_comment:
5
+ types: [created]
6
+ pull_request_review_comment:
7
+ types: [created]
8
+ issues:
9
+ types: [opened, assigned]
10
+ pull_request_review:
11
+ types: [submitted]
12
+
13
+ jobs:
14
+ claude:
15
+ if: |
16
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18
+ (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19
+ (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: read
23
+ pull-requests: read
24
+ issues: read
25
+ id-token: write
26
+ actions: read # Required for Claude to read CI results on PRs
27
+ steps:
28
+ - name: Checkout repository
29
+ uses: actions/checkout@v4
30
+ with:
31
+ fetch-depth: 1
32
+
33
+ - name: Run Claude Code
34
+ id: claude
35
+ uses: anthropics/claude-code-action@beta
36
+ with:
37
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
+
39
+ # This is an optional setting that allows Claude to read CI results on PRs
40
+ additional_permissions: |
41
+ actions: read
42
+
43
+ # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
44
+ # model: "claude-opus-4-20250514"
45
+
46
+ # Optional: Customize the trigger phrase (default: @claude)
47
+ # trigger_phrase: "/claude"
48
+
49
+ # Optional: Trigger when specific user is assigned to an issue
50
+ # assignee_trigger: "claude-bot"
51
+
52
+ # Optional: Allow Claude to run specific commands
53
+ # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
54
+
55
+ # Optional: Add custom instructions for Claude to customize its behavior for your project
56
+ # custom_instructions: |
57
+ # Follow our coding standards
58
+ # Ensure all new code has tests
59
+ # Use TypeScript for new files
60
+
61
+ # Optional: Custom environment variables for Claude
62
+ # claude_env: |
63
+ # NODE_ENV: test
64
+
@@ -4,6 +4,21 @@ All notable changes to SQLSaber will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.13.0] - 2025-07-26
8
+
9
+ ### Added
10
+
11
+ - Database resolver abstraction for unified connection handling
12
+ - Extended `-d` flag to accept PostgreSQL and MySQL connection strings (e.g., `postgresql://user:pass@host:5432/db`)
13
+ - Support for direct connection strings alongside existing file path and configured database support
14
+ - Examples: `saber -d "postgresql://user:pass@host:5432/db" "show users"`
15
+
16
+ ## [0.12.0] - 2025-07-23
17
+
18
+ ### Added
19
+
20
+ - Add support for ad-hoc SQLite files via `--database`/`-d` flag
21
+
7
22
  ## [0.11.0] - 2025-07-09
8
23
 
9
24
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.11.0
3
+ Version: 0.13.0
4
4
  Summary: SQLSaber - Agentic SQL assistant like Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -21,6 +21,15 @@ Description-Content-Type: text/markdown
21
21
 
22
22
  # SQLSaber
23
23
 
24
+ ```
25
+ ███████ ██████ ██ ███████ █████ ██████ ███████ ██████
26
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
27
+ ███████ ██ ██ ██ ███████ ███████ ██████ █████ ██████
28
+ ██ ██ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
29
+ ███████ ██████ ███████ ███████ ██ ██ ██████ ███████ ██ ██
30
+ ▀▀
31
+ ```
32
+
24
33
  > Use the agent Luke!
25
34
 
26
35
  SQLSaber is an agentic SQL assistant. Think Claude Code but for SQL.
@@ -1,5 +1,14 @@
1
1
  # SQLSaber
2
2
 
3
+ ```
4
+ ███████ ██████ ██ ███████ █████ ██████ ███████ ██████
5
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
6
+ ███████ ██ ██ ██ ███████ ███████ ██████ █████ ██████
7
+ ██ ██ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
8
+ ███████ ██████ ███████ ███████ ██ ██ ██████ ███████ ██ ██
9
+ ▀▀
10
+ ```
11
+
3
12
  > Use the agent Luke!
4
13
 
5
14
  SQLSaber is an agentic SQL assistant. Think Claude Code but for SQL.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlsaber"
3
- version = "0.11.0"
3
+ version = "0.13.0"
4
4
  description = "SQLSaber - Agentic SQL assistant like Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -2,7 +2,6 @@
2
2
 
3
3
  import asyncio
4
4
  import sys
5
- from pathlib import Path
6
5
  from typing import Annotated
7
6
 
8
7
  import cyclopts
@@ -17,6 +16,7 @@ from sqlsaber.cli.models import create_models_app
17
16
  from sqlsaber.cli.streaming import StreamingQueryHandler
18
17
  from sqlsaber.config.database import DatabaseConfigManager
19
18
  from sqlsaber.database.connection import DatabaseConnection
19
+ from sqlsaber.database.resolver import DatabaseResolutionError, resolve_database
20
20
 
21
21
 
22
22
  class CLIError(Exception):
@@ -43,7 +43,7 @@ def meta_handler(
43
43
  str | None,
44
44
  cyclopts.Parameter(
45
45
  ["--database", "-d"],
46
- help="Database connection name (uses default if not specified)",
46
+ help="Database connection name, file path (CSV/SQLite), or connection string (postgresql://, mysql://) (uses default if not specified)",
47
47
  ),
48
48
  ] = None,
49
49
  ):
@@ -54,6 +54,10 @@ def meta_handler(
54
54
  saber # Start interactive mode
55
55
  saber "show me all users" # Run a single query with default database
56
56
  saber -d mydb "show me users" # Run a query with specific database
57
+ saber -d data.csv "show me users" # Run a query with ad-hoc CSV file
58
+ saber -d data.db "show me users" # Run a query with ad-hoc SQLite file
59
+ saber -d "postgresql://user:pass@host:5432/db" "show users" # PostgreSQL connection string
60
+ saber -d "mysql://user:pass@host:3306/db" "show users" # MySQL connection string
57
61
  echo "show me all users" | saber # Read query from stdin
58
62
  cat query.txt | saber # Read query from file via stdin
59
63
  """
@@ -73,7 +77,7 @@ def query(
73
77
  str | None,
74
78
  cyclopts.Parameter(
75
79
  ["--database", "-d"],
76
- help="Database connection name (uses default if not specified)",
80
+ help="Database connection name, file path (CSV/SQLite), or connection string (postgresql://, mysql://) (uses default if not specified)",
77
81
  ),
78
82
  ] = None,
79
83
  ):
@@ -88,6 +92,10 @@ def query(
88
92
  Examples:
89
93
  saber # Start interactive mode
90
94
  saber "show me all users" # Run a single query
95
+ saber -d data.csv "show users" # Run a query with ad-hoc CSV file
96
+ saber -d data.db "show users" # Run a query with ad-hoc SQLite file
97
+ saber -d "postgresql://user:pass@host:5432/db" "show users" # PostgreSQL connection string
98
+ saber -d "mysql://user:pass@host:3306/db" "show users" # MySQL connection string
91
99
  echo "show me all users" | saber # Read query from stdin
92
100
  """
93
101
 
@@ -101,32 +109,13 @@ def query(
101
109
  # If stdin was empty, fall back to interactive mode
102
110
  actual_query = None
103
111
 
104
- # Get database configuration or handle direct CSV file
105
- if database:
106
- # Check if this is a direct CSV file path
107
- if database.endswith(".csv"):
108
- csv_path = Path(database).expanduser().resolve()
109
- if not csv_path.exists():
110
- raise CLIError(f"CSV file '{database}' not found.")
111
- connection_string = f"csv:///{csv_path}"
112
- db_name = csv_path.stem
113
- else:
114
- # Look up configured database connection
115
- db_config = config_manager.get_database(database)
116
- if not db_config:
117
- raise CLIError(
118
- f"Database connection '{database}' not found. Use 'sqlsaber db list' to see available connections."
119
- )
120
- connection_string = db_config.to_connection_string()
121
- db_name = db_config.name
122
- else:
123
- db_config = config_manager.get_default_database()
124
- if not db_config:
125
- raise CLIError(
126
- "No database connections configured. Use 'sqlsaber db add <name>' to add a database connection."
127
- )
128
- connection_string = db_config.to_connection_string()
129
- db_name = db_config.name
112
+ # Resolve database from CLI input
113
+ try:
114
+ resolved = resolve_database(database, config_manager)
115
+ connection_string = resolved.connection_string
116
+ db_name = resolved.name
117
+ except DatabaseResolutionError as e:
118
+ raise CLIError(str(e))
130
119
 
131
120
  # Create database connection
132
121
  try:
@@ -0,0 +1,96 @@
1
+ """Database connection resolution from CLI input."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from urllib.parse import urlparse
8
+
9
+ from sqlsaber.config.database import DatabaseConfig, DatabaseConfigManager
10
+
11
+
12
+ class DatabaseResolutionError(Exception):
13
+ """Exception raised when database resolution fails."""
14
+
15
+ pass
16
+
17
+
18
+ @dataclass
19
+ class ResolvedDatabase:
20
+ """Result of database resolution containing canonical connection info."""
21
+
22
+ name: str # Human-readable name for display/logging
23
+ connection_string: str # Canonical connection string for DatabaseConnection factory
24
+
25
+
26
+ SUPPORTED_SCHEMES = {"postgresql", "mysql", "sqlite", "csv"}
27
+
28
+
29
+ def _is_connection_string(s: str) -> bool:
30
+ """Check if string looks like a connection string with supported scheme."""
31
+ try:
32
+ scheme = urlparse(s).scheme
33
+ return scheme in SUPPORTED_SCHEMES
34
+ except Exception:
35
+ return False
36
+
37
+
38
+ def resolve_database(
39
+ spec: str | None, config_mgr: DatabaseConfigManager
40
+ ) -> ResolvedDatabase:
41
+ """Turn user CLI input into resolved database connection info.
42
+
43
+ Args:
44
+ spec: User input - None (default), configured name, connection string, or file path
45
+ config_mgr: Database configuration manager for looking up configured connections
46
+
47
+ Returns:
48
+ ResolvedDatabase with name and canonical connection string
49
+
50
+ Raises:
51
+ DatabaseResolutionError: If the spec cannot be resolved to a valid database connection
52
+ """
53
+ if spec is None:
54
+ db_cfg = config_mgr.get_default_database()
55
+ if not db_cfg:
56
+ raise DatabaseResolutionError(
57
+ "No database connections configured. "
58
+ "Use 'sqlsaber db add <name>' to add one."
59
+ )
60
+ return ResolvedDatabase(
61
+ name=db_cfg.name,
62
+ connection_string=db_cfg.to_connection_string(),
63
+ )
64
+
65
+ # 1. Connection string?
66
+ if _is_connection_string(spec):
67
+ scheme = urlparse(spec).scheme
68
+ if scheme in {"postgresql", "mysql"}:
69
+ db_name = urlparse(spec).path.lstrip("/") or "database"
70
+ elif scheme in {"sqlite", "csv"}:
71
+ db_name = Path(urlparse(spec).path).stem
72
+ else: # should not happen because of SUPPORTED_SCHEMES
73
+ db_name = "database"
74
+ return ResolvedDatabase(name=db_name, connection_string=spec)
75
+
76
+ # 2. Raw file path?
77
+ path = Path(spec).expanduser().resolve()
78
+ if path.suffix.lower() == ".csv":
79
+ if not path.exists():
80
+ raise DatabaseResolutionError(f"CSV file '{spec}' not found.")
81
+ return ResolvedDatabase(name=path.stem, connection_string=f"csv:///{path}")
82
+ if path.suffix.lower() in {".db", ".sqlite", ".sqlite3"}:
83
+ if not path.exists():
84
+ raise DatabaseResolutionError(f"SQLite file '{spec}' not found.")
85
+ return ResolvedDatabase(name=path.stem, connection_string=f"sqlite:///{path}")
86
+
87
+ # 3. Must be a configured name
88
+ db_cfg: DatabaseConfig | None = config_mgr.get_database(spec)
89
+ if not db_cfg:
90
+ raise DatabaseResolutionError(
91
+ f"Database connection '{spec}' not found. "
92
+ "Use 'sqlsaber db list' to see available connections."
93
+ )
94
+ return ResolvedDatabase(
95
+ name=db_cfg.name, connection_string=db_cfg.to_connection_string()
96
+ )
@@ -0,0 +1,126 @@
1
+ """Tests for database resolver functionality."""
2
+
3
+ import pytest
4
+ from unittest.mock import Mock, patch
5
+ from sqlsaber.database.resolver import resolve_database, DatabaseResolutionError
6
+ from sqlsaber.config.database import DatabaseConfig
7
+
8
+
9
+ class TestDatabaseResolver:
10
+ """Test cases for database resolution logic."""
11
+
12
+ def test_resolve_connection_strings(self):
13
+ """Test that connection strings are handled correctly."""
14
+ config_mgr = Mock()
15
+
16
+ # PostgreSQL connection string
17
+ result = resolve_database("postgresql://user:pass@host:5432/testdb", config_mgr)
18
+ assert result.name == "testdb"
19
+ assert result.connection_string == "postgresql://user:pass@host:5432/testdb"
20
+
21
+ # MySQL connection string
22
+ result = resolve_database("mysql://user:pass@host:3306/mydb", config_mgr)
23
+ assert result.name == "mydb"
24
+ assert result.connection_string == "mysql://user:pass@host:3306/mydb"
25
+
26
+ # SQLite connection string
27
+ result = resolve_database("sqlite:///test.db", config_mgr)
28
+ assert result.name == "test"
29
+ assert result.connection_string == "sqlite:///test.db"
30
+
31
+ # CSV connection string
32
+ result = resolve_database("csv:///data.csv", config_mgr)
33
+ assert result.name == "data"
34
+ assert result.connection_string == "csv:///data.csv"
35
+
36
+ @patch("pathlib.Path.exists")
37
+ def test_resolve_file_paths(self, mock_exists):
38
+ """Test that file paths are resolved correctly."""
39
+ mock_exists.return_value = True
40
+ config_mgr = Mock()
41
+
42
+ # CSV file
43
+ result = resolve_database("data.csv", config_mgr)
44
+ assert result.name == "data"
45
+ assert result.connection_string.startswith("csv:///")
46
+ assert result.connection_string.endswith("data.csv")
47
+
48
+ # SQLite file
49
+ result = resolve_database("test.db", config_mgr)
50
+ assert result.name == "test"
51
+ assert result.connection_string.startswith("sqlite:///")
52
+ assert result.connection_string.endswith("test.db")
53
+
54
+ @patch("pathlib.Path.exists")
55
+ def test_file_not_found_error(self, mock_exists):
56
+ """Test that missing files raise appropriate errors."""
57
+ mock_exists.return_value = False
58
+ config_mgr = Mock()
59
+
60
+ with pytest.raises(
61
+ DatabaseResolutionError, match="CSV file 'missing.csv' not found"
62
+ ):
63
+ resolve_database("missing.csv", config_mgr)
64
+
65
+ with pytest.raises(
66
+ DatabaseResolutionError, match="SQLite file 'missing.db' not found"
67
+ ):
68
+ resolve_database("missing.db", config_mgr)
69
+
70
+ def test_resolve_configured_database(self):
71
+ """Test that configured database names are resolved."""
72
+ config_mgr = Mock()
73
+ db_config = Mock(spec=DatabaseConfig)
74
+ db_config.name = "mydb"
75
+ db_config.to_connection_string.return_value = "postgresql://localhost:5432/mydb"
76
+ config_mgr.get_database.return_value = db_config
77
+
78
+ result = resolve_database("mydb", config_mgr)
79
+ assert result.name == "mydb"
80
+ assert result.connection_string == "postgresql://localhost:5432/mydb"
81
+
82
+ def test_configured_database_not_found(self):
83
+ """Test error when configured database doesn't exist."""
84
+ config_mgr = Mock()
85
+ config_mgr.get_database.return_value = None
86
+
87
+ with pytest.raises(
88
+ DatabaseResolutionError, match="Database connection 'unknown' not found"
89
+ ):
90
+ resolve_database("unknown", config_mgr)
91
+
92
+ def test_resolve_default_database(self):
93
+ """Test that None resolves to default database."""
94
+ config_mgr = Mock()
95
+ db_config = Mock(spec=DatabaseConfig)
96
+ db_config.name = "default"
97
+ db_config.to_connection_string.return_value = (
98
+ "postgresql://localhost:5432/default"
99
+ )
100
+ config_mgr.get_default_database.return_value = db_config
101
+
102
+ result = resolve_database(None, config_mgr)
103
+ assert result.name == "default"
104
+ assert result.connection_string == "postgresql://localhost:5432/default"
105
+
106
+ def test_no_default_database_error(self):
107
+ """Test error when no default database is configured."""
108
+ config_mgr = Mock()
109
+ config_mgr.get_default_database.return_value = None
110
+
111
+ with pytest.raises(
112
+ DatabaseResolutionError, match="No database connections configured"
113
+ ):
114
+ resolve_database(None, config_mgr)
115
+
116
+ def test_connection_string_edge_cases(self):
117
+ """Test edge cases in connection string parsing."""
118
+ config_mgr = Mock()
119
+
120
+ # PostgreSQL without database name
121
+ result = resolve_database("postgresql://user:pass@host:5432/", config_mgr)
122
+ assert result.name == "database" # fallback name
123
+
124
+ # PostgreSQL with no path at all
125
+ result = resolve_database("postgresql://user:pass@host:5432", config_mgr)
126
+ assert result.name == "database" # fallback name
@@ -918,7 +918,7 @@ wheels = [
918
918
 
919
919
  [[package]]
920
920
  name = "sqlsaber"
921
- version = "0.11.0"
921
+ version = "0.13.0"
922
922
  source = { editable = "." }
923
923
  dependencies = [
924
924
  { name = "aiomysql" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes