sqlalchemy-excel 0.2.2__tar.gz → 0.3.1__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.
Files changed (49) hide show
  1. sqlalchemy_excel-0.3.1/CHANGELOG.md +72 -0
  2. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/PKG-INFO +30 -1
  3. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/README.md +27 -0
  4. sqlalchemy_excel-0.3.1/docs/DEVELOPMENT.md +263 -0
  5. sqlalchemy_excel-0.3.1/docs/ROADMAP.md +151 -0
  6. sqlalchemy_excel-0.3.1/docs/USAGE.md +267 -0
  7. sqlalchemy_excel-0.3.1/logo.svg +21 -0
  8. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/pyproject.toml +3 -1
  9. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/src/sqlalchemy_excel/__init__.py +3 -2
  10. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/src/sqlalchemy_excel/dialect.py +61 -0
  11. sqlalchemy_excel-0.3.1/tests/test_graph_dialect.py +170 -0
  12. sqlalchemy_excel-0.2.2/CHANGELOG.md +0 -13
  13. sqlalchemy_excel-0.2.2/logo.svg +0 -23
  14. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.editorconfig +0 -0
  15. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  16. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  17. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  18. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/ISSUE_TEMPLATE/task.yml +0 -0
  19. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  20. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/RELEASE_CHECKLIST.md +0 -0
  21. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/RELEASE_NOTES_TEMPLATE.md +0 -0
  22. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/dependabot.yml +0 -0
  23. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/labels.yml +0 -0
  24. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/release.yml +0 -0
  25. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/workflows/ci.yml +0 -0
  26. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/workflows/publish-pypi.yml +0 -0
  27. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.gitignore +0 -0
  28. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.pre-commit-config.yaml +0 -0
  29. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/CODE_OF_CONDUCT.md +0 -0
  30. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/CONTRIBUTING.md +0 -0
  31. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/LICENSE +0 -0
  32. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/Makefile +0 -0
  33. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/SECURITY.md +0 -0
  34. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/SUPPORT.md +0 -0
  35. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/cliff.toml +0 -0
  36. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/codecov.yml +0 -0
  37. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/src/sqlalchemy_excel/compiler.py +0 -0
  38. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/src/sqlalchemy_excel/ddl.py +0 -0
  39. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/src/sqlalchemy_excel/py.typed +0 -0
  40. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/src/sqlalchemy_excel/reflection.py +0 -0
  41. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/src/sqlalchemy_excel/types.py +0 -0
  42. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/conftest.py +0 -0
  43. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/test_compiler.py +0 -0
  44. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/test_ddl.py +0 -0
  45. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/test_dialect.py +0 -0
  46. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/test_dml.py +0 -0
  47. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/test_orm.py +0 -0
  48. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/test_reflection.py +0 -0
  49. {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/test_types.py +0 -0
@@ -0,0 +1,72 @@
1
+ # Changelog
2
+
3
+ ## [0.3.1] - 2026-04-12
4
+
5
+ ### Fixed
6
+ - Add explicit `supports_statement_cache = False` to `ExcelGraphDialect` to suppress SQLAlchemy caching warning
7
+ - Fix import ordering in test_graph_dialect.py for ruff I001 compliance
8
+
9
+ ## [0.3.0] - 2026-04-12
10
+
11
+ ### Added
12
+ - `ExcelGraphDialect` for remote Excel files via Microsoft Graph API
13
+ - `excel+graph:///drive_id/item_id` URL scheme support
14
+ - Entry point `excel.graph` for SQLAlchemy dialect resolution
15
+ - Optional dependency: `pip install sqlalchemy-excel[graph]`
16
+ - URL percent-decoding for drive/item IDs with special characters
17
+ - `readonly` query parameter forwarding to Graph backend
18
+ - Comprehensive Graph dialect tests with `httpx.MockTransport`
19
+ - `docs/` directory with USAGE.md, DEVELOPMENT.md, and ROADMAP.md
20
+
21
+ ### Changed
22
+ - Version bumped to 0.3.0
23
+
24
+ ## [0.2.2] - 2026-04-12
25
+
26
+ ### Fixed
27
+ - Restored cast() calls needed for CI mypy and suppress redundant-cast locally
28
+ - Version bumped to 0.2.2
29
+
30
+ ## [0.2.1] - 2026-04-12
31
+
32
+ ### Added
33
+ - Project logo (modern minimalist SVG)
34
+ - Contributing guide, Code of Conduct, Security and Support policies
35
+ - Development tooling: Makefile, .editorconfig, pre-commit-config, codecov.yml, git-cliff config
36
+ - GitHub issue/PR templates and project management files
37
+ - py.typed marker for PEP 561 compliance
38
+ - twine check step in publish workflow
39
+
40
+ ### Changed
41
+ - Classifier updated from Alpha to Beta
42
+ - Changelog URL added to project metadata
43
+
44
+ ### Fixed
45
+ - Oracle review findings: rollback docs, absolute logo URLs, metadata alignment
46
+
47
+ ## [0.2.0] - 2026-04-12
48
+
49
+ ### Added
50
+ - Full dialect rewrite: ExcelCompiler, ExcelDDLCompiler, ExcelTypeCompiler, ExcelInspectionMixin
51
+ - Comprehensive README with ORM examples, type mapping table, schema inspection docs
52
+ - Test coverage reporting with Codecov CI integration
53
+ - IN, BETWEEN, LIKE operator tests for SQLAlchemy dialect
54
+
55
+ ### Changed
56
+ - excel-dbapi dependency updated to >=0.2.0
57
+ - Version bumped to 0.2.0
58
+
59
+ ### Fixed
60
+ - mypy strict incompatibility with SQLAlchemy dialect overrides (temporarily disabled then re-enabled)
61
+
62
+ ## [0.1.0] - 2026-04-12
63
+
64
+ - Initial release
65
+ - SQLAlchemy 2.0 dialect for Excel files
66
+ - PEP 249 DB-API 2.0 driver via excel-dbapi
67
+ - SQL support: SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE
68
+ - WHERE clause with AND/OR, comparison operators, IS NULL, IS NOT NULL
69
+ - ORDER BY, LIMIT
70
+ - Type mapping: TEXT, INTEGER, FLOAT, BOOLEAN, DATE, DATETIME
71
+ - Reflection: get_table_names, get_columns, get_pk_constraint, has_table
72
+ - ORM support with DeclarativeBase
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlalchemy-excel
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: SQLAlchemy dialect for Excel files — use Excel as a database
5
5
  Project-URL: Homepage, https://github.com/yeongseon/sqlalchemy-excel
6
6
  Project-URL: Repository, https://github.com/yeongseon/sqlalchemy-excel
@@ -29,6 +29,8 @@ Requires-Dist: mypy>=1.10; extra == 'dev'
29
29
  Requires-Dist: pytest-cov>=4.0; extra == 'dev'
30
30
  Requires-Dist: pytest>=8.0; extra == 'dev'
31
31
  Requires-Dist: ruff>=0.4; extra == 'dev'
32
+ Provides-Extra: graph
33
+ Requires-Dist: excel-dbapi[graph]>=0.2.0; extra == 'graph'
32
34
  Description-Content-Type: text/markdown
33
35
 
34
36
  <p align="left">
@@ -89,6 +91,33 @@ engine = create_engine("excel:////home/user/data.xlsx")
89
91
  engine = create_engine("excel:///data.xlsx", connect_args={"engine": "openpyxl"})
90
92
  ```
91
93
 
94
+ ## Remote Excel via Microsoft Graph API
95
+
96
+ Access Excel files on OneDrive/SharePoint directly:
97
+
98
+ ```bash
99
+ pip install sqlalchemy-excel[graph]
100
+ ```
101
+
102
+ ```python
103
+ from sqlalchemy import create_engine
104
+ from azure.identity import DefaultAzureCredential
105
+
106
+ engine = create_engine(
107
+ "excel+graph:///drive_id/item_id",
108
+ connect_args={"credential": DefaultAzureCredential()},
109
+ )
110
+
111
+ with engine.connect() as conn:
112
+ result = conn.execute(text("SELECT * FROM Sheet1"))
113
+ for row in result:
114
+ print(row)
115
+ ```
116
+
117
+ URL format: `excel+graph:///drive_id/item_id` where `drive_id` and `item_id` are Microsoft Graph resource identifiers.
118
+ Query parameters: `?readonly=false` to enable write operations.
119
+
120
+
92
121
  ## Features
93
122
 
94
123
  - Full SQLAlchemy 2.0 dialect
@@ -56,6 +56,33 @@ engine = create_engine("excel:////home/user/data.xlsx")
56
56
  engine = create_engine("excel:///data.xlsx", connect_args={"engine": "openpyxl"})
57
57
  ```
58
58
 
59
+ ## Remote Excel via Microsoft Graph API
60
+
61
+ Access Excel files on OneDrive/SharePoint directly:
62
+
63
+ ```bash
64
+ pip install sqlalchemy-excel[graph]
65
+ ```
66
+
67
+ ```python
68
+ from sqlalchemy import create_engine
69
+ from azure.identity import DefaultAzureCredential
70
+
71
+ engine = create_engine(
72
+ "excel+graph:///drive_id/item_id",
73
+ connect_args={"credential": DefaultAzureCredential()},
74
+ )
75
+
76
+ with engine.connect() as conn:
77
+ result = conn.execute(text("SELECT * FROM Sheet1"))
78
+ for row in result:
79
+ print(row)
80
+ ```
81
+
82
+ URL format: `excel+graph:///drive_id/item_id` where `drive_id` and `item_id` are Microsoft Graph resource identifiers.
83
+ Query parameters: `?readonly=false` to enable write operations.
84
+
85
+
59
86
  ## Features
60
87
 
61
88
  - Full SQLAlchemy 2.0 dialect
@@ -0,0 +1,263 @@
1
+ # Development Guide
2
+
3
+ This guide covers how to set up a development environment and contribute to sqlalchemy-excel.
4
+
5
+ ## Getting Started
6
+
7
+ ### Clone the Repository
8
+
9
+ ```bash
10
+ git clone https://github.com/yeongseon/sqlalchemy-excel.git
11
+ cd sqlalchemy-excel
12
+ ```
13
+
14
+ ### Set Up Virtual Environment
15
+
16
+ Create and activate a virtual environment:
17
+
18
+ ```bash
19
+ # Using venv (Python 3.10+)
20
+ python -m venv .venv
21
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
22
+
23
+ # Or using virtualenv
24
+ virtualenv .venv
25
+ source .venv/bin/activate
26
+ ```
27
+
28
+ ### Install Development Dependencies
29
+
30
+ Install the package in editable mode with development dependencies:
31
+
32
+ ```bash
33
+ pip install -e ".[dev]"
34
+ ```
35
+
36
+ This installs:
37
+ - `sqlalchemy>=2.0`
38
+ - `excel-dbapi>=0.2.0`
39
+ - `pytest>=8.0`
40
+ - `pytest-cov>=4.0`
41
+ - `ruff>=0.4`
42
+ - `mypy>=1.10`
43
+
44
+ ## Makefile Commands
45
+
46
+ The project includes a Makefile for common development tasks:
47
+
48
+ | Command | Description |
49
+ |---------|-------------|
50
+ | `make install` | Install the package in editable mode with dev dependencies |
51
+ | `make format` | Format code with ruff |
52
+ | `make lint` | Run linting checks with ruff and mypy |
53
+ | `make test` | Run tests with pytest |
54
+ | `make coverage` | Run tests with coverage report |
55
+ | `make build` | Build distribution packages |
56
+ | `make clean` | Remove build artifacts and cache files |
57
+
58
+ ## Running Tests
59
+
60
+ Run the test suite using pytest:
61
+
62
+ ```bash
63
+ # Run all tests
64
+ pytest
65
+
66
+ # Or use make
67
+ make test
68
+
69
+ # Run with coverage
70
+ pytest --cov=sqlalchemy_excel --cov-report=html
71
+ make coverage
72
+
73
+ # Run specific test file
74
+ pytest tests/test_dialect.py
75
+
76
+ # Run specific test
77
+ pytest tests/test_dialect.py::test_create_engine
78
+
79
+ # Run with verbose output
80
+ pytest -v
81
+ ```
82
+
83
+ Test files are located in the `tests/` directory.
84
+
85
+ ## Code Style
86
+
87
+ sqlalchemy-excel follows strict code quality standards:
88
+
89
+ ### Linting and Formatting
90
+
91
+ **Ruff** is used for both linting and formatting:
92
+
93
+ ```bash
94
+ # Format code (auto-fix)
95
+ ruff format .
96
+
97
+ # Check linting (without fixing)
98
+ ruff check .
99
+
100
+ # Auto-fix linting issues
101
+ ruff check --fix .
102
+
103
+ # Or use make
104
+ make format # Format + auto-fix
105
+ make lint # Check without fixing
106
+ ```
107
+
108
+ Configuration is in `pyproject.toml`:
109
+ - Target version: Python 3.10+
110
+ - Line length: 88 characters
111
+ - Enabled rules: pycodestyle, pyflakes, isort, pep8-naming, pyupgrade, flake8-bugbear, flake8-simplify, flake8-type-checking, ruff-specific
112
+
113
+ ### Type Checking
114
+
115
+ **mypy** is configured in strict mode:
116
+
117
+ ```bash
118
+ mypy src/sqlalchemy_excel
119
+
120
+ # Or as part of make lint
121
+ make lint
122
+ ```
123
+
124
+ Configuration (`pyproject.toml`):
125
+ - `strict = true`
126
+ - `warn_return_any = true`
127
+ - `warn_unused_configs = true`
128
+
129
+ All code must pass strict type checking before merging.
130
+
131
+ ## Project Structure
132
+
133
+ ```
134
+ sqlalchemy-excel/
135
+ ├── src/sqlalchemy_excel/ # Main source code
136
+ │ ├── __init__.py # Package entry point
137
+ │ ├── dialect.py # ExcelDialect implementation
138
+ │ ├── compiler.py # SQL compilation (ExcelCompiler, DDLCompiler, TypeCompiler)
139
+ │ ├── types.py # Type mappings
140
+ │ └── py.typed # PEP 561 marker file
141
+ ├── tests/ # Test suite
142
+ │ ├── test_dialect.py
143
+ │ ├── test_compiler.py
144
+ │ └── fixtures/ # Test data files
145
+ ├── docs/ # Documentation
146
+ │ ├── USAGE.md
147
+ │ ├── DEVELOPMENT.md
148
+ │ └── ROADMAP.md
149
+ ├── pyproject.toml # Project metadata and config
150
+ ├── README.md # Main documentation
151
+ ├── CHANGELOG.md # Version history
152
+ ├── LICENSE # MIT License
153
+ └── Makefile # Development commands
154
+ ```
155
+
156
+ ## Development Workflow
157
+
158
+ 1. **Create a feature branch**:
159
+ ```bash
160
+ git checkout -b feature/my-feature
161
+ ```
162
+
163
+ 2. **Make changes** and add tests
164
+
165
+ 3. **Format and lint**:
166
+ ```bash
167
+ make format
168
+ make lint
169
+ ```
170
+
171
+ 4. **Run tests**:
172
+ ```bash
173
+ make test
174
+ ```
175
+
176
+ 5. **Commit changes**:
177
+ ```bash
178
+ git add .
179
+ git commit -m "feat: add new feature"
180
+ ```
181
+
182
+ 6. **Push and create pull request**:
183
+ ```bash
184
+ git push origin feature/my-feature
185
+ ```
186
+
187
+ ## Release Process
188
+
189
+ sqlalchemy-excel uses GitHub Actions for automated releases:
190
+
191
+ ### 1. Update CHANGELOG.md
192
+
193
+ Document all changes in the changelog following the format:
194
+
195
+ ```markdown
196
+ ## [0.3.0] - 2024-01-15
197
+
198
+ ### Added
199
+ - New feature X
200
+ - Support for Y
201
+
202
+ ### Fixed
203
+ - Bug Z
204
+ ```
205
+
206
+ ### 2. Bump Version
207
+
208
+ Update the version in `pyproject.toml`:
209
+
210
+ ```toml
211
+ [project]
212
+ version = "0.3.0"
213
+ ```
214
+
215
+ ### 3. Create Git Tag
216
+
217
+ ```bash
218
+ git add pyproject.toml CHANGELOG.md
219
+ git commit -m "chore: bump version to 0.3.0"
220
+ git tag v0.3.0
221
+ git push origin main
222
+ git push origin v0.3.0
223
+ ```
224
+
225
+ ### 4. Automated Publishing
226
+
227
+ When you push a tag (`v*`), GitHub Actions automatically:
228
+ 1. Runs all tests and linting
229
+ 2. Builds distribution packages (`sdist` and `wheel`)
230
+ 3. Publishes to PyPI using **Trusted Publisher (OIDC)**
231
+
232
+ **No API token needed** — the project uses PyPI's Trusted Publisher feature with OIDC authentication configured in GitHub Actions.
233
+
234
+ ### 5. Verify Release
235
+
236
+ Check that the release appears on:
237
+ - PyPI: https://pypi.org/project/sqlalchemy-excel/
238
+ - GitHub Releases: https://github.com/yeongseon/sqlalchemy-excel/releases
239
+
240
+ ## Continuous Integration
241
+
242
+ The CI pipeline (`.github/workflows/ci.yml`) runs on every push and pull request:
243
+
244
+ 1. **Linting**: ruff + mypy
245
+ 2. **Testing**: pytest on Python 3.10, 3.11, 3.12, 3.13
246
+ 3. **Coverage**: Upload to Codecov
247
+
248
+ All checks must pass before merging.
249
+
250
+ ## Contributing Guidelines
251
+
252
+ - Write tests for all new features and bug fixes
253
+ - Maintain or improve code coverage (target: 90%+)
254
+ - Follow the existing code style (enforced by ruff)
255
+ - Add type hints for all functions (enforced by mypy strict mode)
256
+ - Update documentation for user-facing changes
257
+ - Keep commits atomic and write clear commit messages
258
+
259
+ ## Getting Help
260
+
261
+ - Open an issue: https://github.com/yeongseon/sqlalchemy-excel/issues
262
+ - Check existing discussions and issues
263
+ - Review the main README and USAGE guide
@@ -0,0 +1,151 @@
1
+ # Project Roadmap
2
+
3
+ This roadmap outlines the past achievements and future plans for sqlalchemy-excel.
4
+
5
+ ## Completed Features
6
+
7
+ ### v0.1.0 — Initial Release
8
+ - ✅ SQLAlchemy 2.0 dialect implementation
9
+ - ✅ ORM support with `DeclarativeBase`
10
+ - ✅ Basic SQL operations: SELECT, INSERT, UPDATE, DELETE
11
+ - ✅ WHERE clauses with comparison operators
12
+ - ✅ ORDER BY and LIMIT support
13
+ - ✅ Type mapping for common SQLAlchemy types
14
+ - ✅ Integration with excel-dbapi driver
15
+ - ✅ Schema inspection (`get_table_names`, `get_columns`, `has_table`)
16
+
17
+ ### v0.2.x — Dialect Rewrite and Quality Improvements
18
+ - ✅ Complete dialect architecture rewrite
19
+ - ExcelCompiler for SQL compilation
20
+ - DDLCompiler for CREATE/DROP TABLE
21
+ - TypeCompiler for type system
22
+ - ✅ Enhanced operator support: IN, BETWEEN, LIKE
23
+ - ✅ Codecov integration for coverage tracking
24
+ - ✅ mypy strict mode with full type safety
25
+ - ✅ Comprehensive test suite
26
+ - ✅ Improved documentation and examples
27
+ - ✅ GitHub Actions CI/CD pipeline
28
+ - ✅ PyPI Trusted Publisher (OIDC) for secure releases
29
+
30
+ ## Planned Features
31
+
32
+ ### High Priority
33
+
34
+ #### Remote Excel Access via Microsoft Graph API
35
+ - [ ] Implement `excel+graph://` URL scheme
36
+ - [ ] Support for OneDrive and SharePoint Excel files
37
+ - [ ] OAuth 2.0 authentication flow
38
+ - [ ] Read/write operations on cloud-stored Excel files
39
+ - [ ] Caching layer for remote files
40
+
41
+ **Use case**: Access and query Excel files stored in Microsoft 365 without downloading them locally.
42
+
43
+ ```python
44
+ # Future API
45
+ engine = create_engine(
46
+ "excel+graph:///sites/mysite/documents/data.xlsx",
47
+ connect_args={
48
+ "tenant_id": "...",
49
+ "client_id": "...",
50
+ "client_secret": "..."
51
+ }
52
+ )
53
+ ```
54
+
55
+ ### Medium Priority
56
+
57
+ #### Advanced SQL Support
58
+ - [ ] **DISTINCT**: Remove duplicate rows
59
+ - [ ] **OFFSET**: Pagination support (currently only LIMIT works)
60
+ - [ ] **Aggregate functions**: COUNT, SUM, AVG, MIN, MAX
61
+ - [ ] **GROUP BY**: Grouping and aggregation
62
+ - [ ] **HAVING**: Filtering on aggregated data
63
+ - [ ] **Subqueries**: Nested SELECT statements
64
+ - [ ] **CTEs (Common Table Expressions)**: WITH clauses
65
+
66
+ **Status**: These features require significant changes to the excel-dbapi query engine. Aggregate functions are particularly complex due to Excel's storage model.
67
+
68
+ #### Multi-Table Operations
69
+ - [ ] **JOIN support**: INNER JOIN, LEFT JOIN, RIGHT JOIN
70
+ - [ ] Cross-sheet queries
71
+ - [ ] Foreign key awareness (metadata only, no enforcement)
72
+
73
+ **Challenge**: Excel has no native concept of relationships or joins. Implementation would require loading and joining data in memory.
74
+
75
+ #### Performance Optimization
76
+ - [ ] Lazy loading for large Excel files
77
+ - [ ] Column-level filtering (avoid loading entire rows)
78
+ - [ ] Query result caching
79
+ - [ ] Batch operation optimization
80
+ - [ ] Memory-efficient streaming for large datasets
81
+
82
+ **Target**: Support Excel files with 100K+ rows without excessive memory usage.
83
+
84
+ ### Low Priority
85
+
86
+ #### Async Support
87
+ - [ ] Async dialect (`excel+aio://`)
88
+ - [ ] AsyncEngine and AsyncSession support
89
+ - [ ] Non-blocking I/O for file operations
90
+
91
+ **Note**: Requires asyncio-compatible openpyxl wrapper or alternative Excel library.
92
+
93
+ #### Additional Features
94
+ - [ ] Support for Excel formulas in queries
95
+ - [ ] Worksheet-level transactions (via temporary files)
96
+ - [ ] ALTER TABLE support (add/remove columns)
97
+ - [ ] Index simulation for faster lookups
98
+ - [ ] Excel template support (preserve formatting)
99
+ - [ ] Multiple sheet joins within same file
100
+
101
+ ## Known Issues and Limitations
102
+
103
+ ### Current Limitations (By Design)
104
+ - No transactional rollback (Excel files don't support ACID transactions)
105
+ - No concurrent writes (Excel file format limitations)
106
+ - Limited SQL feature set compared to traditional RDBMS
107
+ - Performance degrades with very large files (>50MB)
108
+
109
+ ### Under Consideration
110
+ - **Alternative Excel engines**: Support for xlrd, xlwt, pyexcel in addition to openpyxl
111
+ - **CSV fallback**: Automatic conversion to CSV for read-only operations
112
+ - **SQLite hybrid mode**: Use SQLite as intermediate cache for complex queries
113
+
114
+ ## Community Feedback
115
+
116
+ We welcome feedback on this roadmap! Please:
117
+ - 🌟 Star the repository if you find it useful
118
+ - 💬 Open an issue to suggest new features
119
+ - 🐛 Report bugs and edge cases
120
+ - 📝 Contribute to documentation improvements
121
+ - 🔀 Submit pull requests for planned features
122
+
123
+ **Priority is driven by community demand** — let us know what you need!
124
+
125
+ ## Versioning Strategy
126
+
127
+ sqlalchemy-excel follows [Semantic Versioning](https://semver.org/):
128
+
129
+ - **MAJOR (1.0.0)**: Stable API, production-ready, breaking changes
130
+ - **MINOR (0.x.0)**: New features, backward-compatible
131
+ - **PATCH (0.0.x)**: Bug fixes, no new features
132
+
133
+ **Current status**: Beta (0.x.x) — API may change before 1.0.0.
134
+
135
+ ## Long-Term Vision
136
+
137
+ The goal of sqlalchemy-excel is to:
138
+ 1. Provide a **seamless SQLAlchemy experience** for Excel files
139
+ 2. Enable **analysts and developers** to use familiar SQL tools with Excel
140
+ 3. Bridge the gap between **ad-hoc Excel data** and **structured database workflows**
141
+ 4. Support **cloud-native Excel** (Microsoft 365, Google Sheets in future)
142
+
143
+ **Not a goal**: Replace traditional databases for production workloads. Excel is great for prototyping, data analysis, and small-scale applications, but RDBMS should be used for critical systems.
144
+
145
+ ## Contributing to the Roadmap
146
+
147
+ See [DEVELOPMENT.md](DEVELOPMENT.md) for contribution guidelines.
148
+
149
+ ---
150
+
151
+ **Last updated**: 2024-01 (v0.2.2)
@@ -0,0 +1,267 @@
1
+ # Usage Guide
2
+
3
+ This guide covers how to use sqlalchemy-excel in your projects.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install sqlalchemy-excel
9
+ ```
10
+
11
+ The underlying driver `excel-dbapi` is automatically installed as a dependency.
12
+
13
+ ## URL Format
14
+
15
+ sqlalchemy-excel uses the `excel://` URL scheme:
16
+
17
+ ```python
18
+ from sqlalchemy import create_engine
19
+
20
+ # Relative path (relative to current working directory)
21
+ engine = create_engine("excel:///data.xlsx")
22
+
23
+ # Absolute path (note four slashes total: excel:// + //)
24
+ engine = create_engine("excel:////home/user/data.xlsx")
25
+ engine = create_engine("excel:////Users/alice/Documents/data.xlsx")
26
+
27
+ # With engine options (passed to connect_args)
28
+ engine = create_engine("excel:///data.xlsx", connect_args={"engine": "openpyxl"})
29
+ ```
30
+
31
+ **Important**: Absolute paths require **four slashes** total (`excel:////absolute/path.xlsx`).
32
+
33
+ ## Basic ORM Usage
34
+
35
+ ### Define Models
36
+
37
+ ```python
38
+ from sqlalchemy import create_engine
39
+ from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column
40
+
41
+ engine = create_engine("excel:///data.xlsx")
42
+
43
+ class Base(DeclarativeBase):
44
+ pass
45
+
46
+ class User(Base):
47
+ __tablename__ = "users"
48
+ id: Mapped[int] = mapped_column(primary_key=True)
49
+ name: Mapped[str] = mapped_column()
50
+ age: Mapped[int] = mapped_column()
51
+
52
+ # Create the table (sheet) if it doesn't exist
53
+ Base.metadata.create_all(engine)
54
+ ```
55
+
56
+ ### Use Sessions
57
+
58
+ ```python
59
+ # Insert data
60
+ with Session(engine) as session:
61
+ session.add(User(id=1, name="Alice", age=30))
62
+ session.add(User(id=2, name="Bob", age=25))
63
+ session.commit()
64
+
65
+ # Query data
66
+ with Session(engine) as session:
67
+ users = session.query(User).all()
68
+ for user in users:
69
+ print(f"{user.id}: {user.name} ({user.age})")
70
+ ```
71
+
72
+ ## Core Usage
73
+
74
+ You can also use SQLAlchemy Core with `text()` queries:
75
+
76
+ ```python
77
+ from sqlalchemy import create_engine, text
78
+
79
+ engine = create_engine("excel:///data.xlsx")
80
+
81
+ with engine.connect() as conn:
82
+ # Execute a raw SQL query
83
+ result = conn.execute(text("SELECT * FROM Sheet1"))
84
+ for row in result:
85
+ print(row)
86
+
87
+ # Parameterized query (always use this for security)
88
+ result = conn.execute(
89
+ text("SELECT * FROM users WHERE name = :name"),
90
+ {"name": "Alice"}
91
+ )
92
+ for row in result:
93
+ print(row)
94
+ ```
95
+
96
+ ## Query Examples
97
+
98
+ ### WHERE Clause
99
+
100
+ ```python
101
+ from sqlalchemy import select
102
+
103
+ with Session(engine) as session:
104
+ # Simple equality
105
+ user = session.query(User).filter(User.name == "Alice").first()
106
+
107
+ # Comparison operators
108
+ stmt = select(User).where(User.age > 25)
109
+ users = session.scalars(stmt).all()
110
+ ```
111
+
112
+ ### IN Operator
113
+
114
+ ```python
115
+ with Session(engine) as session:
116
+ stmt = select(User).where(User.name.in_(["Alice", "Bob", "Charlie"]))
117
+ users = session.scalars(stmt).all()
118
+ ```
119
+
120
+ ### BETWEEN Operator
121
+
122
+ ```python
123
+ with Session(engine) as session:
124
+ # Find users with age between 25 and 35
125
+ stmt = select(User).where(User.age.between(25, 35))
126
+ users = session.scalars(stmt).all()
127
+ ```
128
+
129
+ ### LIKE Operator
130
+
131
+ ```python
132
+ with Session(engine) as session:
133
+ # Find users whose name starts with 'A'
134
+ stmt = select(User).where(User.name.like("A%"))
135
+ users = session.scalars(stmt).all()
136
+
137
+ # Contains 'li'
138
+ stmt = select(User).where(User.name.like("%li%"))
139
+ users = session.scalars(stmt).all()
140
+ ```
141
+
142
+ ### ORDER BY and LIMIT
143
+
144
+ ```python
145
+ with Session(engine) as session:
146
+ # Order by age descending
147
+ stmt = select(User).order_by(User.age.desc())
148
+ users = session.scalars(stmt).all()
149
+
150
+ # Get top 5 oldest users
151
+ stmt = select(User).order_by(User.age.desc()).limit(5)
152
+ users = session.scalars(stmt).all()
153
+ ```
154
+
155
+ ## Insert, Update, and Delete
156
+
157
+ ### Insert
158
+
159
+ ```python
160
+ with Session(engine) as session:
161
+ new_user = User(id=3, name="Charlie", age=28)
162
+ session.add(new_user)
163
+ session.commit()
164
+ ```
165
+
166
+ ### Update
167
+
168
+ ```python
169
+ with Session(engine) as session:
170
+ user = session.query(User).filter(User.id == 1).first()
171
+ if user:
172
+ user.name = "Ann"
173
+ user.age = 31
174
+ session.commit()
175
+ ```
176
+
177
+ ### Delete
178
+
179
+ ```python
180
+ with Session(engine) as session:
181
+ user = session.query(User).filter(User.id == 2).first()
182
+ if user:
183
+ session.delete(user)
184
+ session.commit()
185
+ ```
186
+
187
+ ## Schema Inspection
188
+
189
+ SQLAlchemy's inspector API works with Excel files:
190
+
191
+ ```python
192
+ from sqlalchemy import create_engine, inspect
193
+
194
+ engine = create_engine("excel:///data.xlsx")
195
+ inspector = inspect(engine)
196
+
197
+ # List all sheets (tables)
198
+ table_names = inspector.get_table_names()
199
+ print(f"Available sheets: {table_names}")
200
+
201
+ # Get columns for a specific sheet
202
+ columns = inspector.get_columns("users")
203
+ for col in columns:
204
+ print(f"{col['name']}: {col['type']}")
205
+
206
+ # Check if a sheet exists
207
+ if inspector.has_table("users"):
208
+ print("The 'users' sheet exists")
209
+ ```
210
+
211
+ ## Type Mapping
212
+
213
+ sqlalchemy-excel maps SQLAlchemy types to Excel storage types:
214
+
215
+ | SQLAlchemy Type | Excel Storage | Notes |
216
+ |-----------------|---------------|-------|
217
+ | `String`, `Text`, `VARCHAR`, `CHAR` | TEXT | All string types → TEXT |
218
+ | `Integer`, `SmallInteger`, `BigInteger` | INTEGER | All integer types → INTEGER |
219
+ | `Float`, `Numeric`, `Decimal` | FLOAT | All numeric types → FLOAT |
220
+ | `Boolean` | BOOLEAN | Stored as boolean |
221
+ | `Date` | DATE | Date without time |
222
+ | `DateTime`, `TIMESTAMP` | DATETIME | Date with time |
223
+ | `Time` | TEXT | Stored as text |
224
+ | `Uuid` | TEXT | Stored as text |
225
+
226
+ **Unsupported types**: BLOB, BINARY, JSON, ARRAY (will raise `CompileError`)
227
+
228
+ ## Limitations
229
+
230
+ sqlalchemy-excel has some limitations due to the nature of Excel as a database:
231
+
232
+ - **No JOIN operations**: Single-table queries only
233
+ - **No GROUP BY, HAVING, DISTINCT**: Aggregations not supported
234
+ - **No OFFSET**: Only LIMIT is supported
235
+ - **No subqueries or CTEs**: Simple queries only
236
+ - **No aggregate functions**: COUNT, SUM, AVG, etc. not available
237
+ - **No ALTER TABLE**: Cannot modify table structure after creation
238
+ - **No foreign keys or indexes**: Excel has no concept of these
239
+ - **No concurrent writes**: Use a single-writer model
240
+ - **Rollback is a no-op**: `Session.rollback()` does nothing — Excel files don't support transactional rollback
241
+
242
+ ## Security
243
+
244
+ **Always use parameterized queries** to prevent SQL injection:
245
+
246
+ ```python
247
+ # ✅ GOOD: Parameterized query
248
+ with engine.connect() as conn:
249
+ result = conn.execute(
250
+ text("SELECT * FROM users WHERE name = :name"),
251
+ {"name": user_input}
252
+ )
253
+
254
+ # ❌ BAD: String interpolation (vulnerable to SQL injection)
255
+ with engine.connect() as conn:
256
+ result = conn.execute(
257
+ text(f"SELECT * FROM users WHERE name = '{user_input}'")
258
+ )
259
+ ```
260
+
261
+ SQLAlchemy ORM queries are automatically parameterized and safe:
262
+
263
+ ```python
264
+ # ✅ SAFE: ORM automatically parameterizes
265
+ with Session(engine) as session:
266
+ user = session.query(User).filter(User.name == user_input).first()
267
+ ```
@@ -0,0 +1,21 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0%" stop-color="#1DB954"/>
5
+ <stop offset="100%" stop-color="#148A40"/>
6
+ </linearGradient>
7
+ </defs>
8
+
9
+ <!-- Base (same as excel-dbapi) -->
10
+ <rect x="8" y="8" width="112" height="112" rx="26" fill="url(#bg)"/>
11
+
12
+ <!-- Left: bold SA angle bracket -->
13
+ <path d="M52 30 L26 64 L52 98" fill="none" stroke="#fff" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" opacity="0.92"/>
14
+
15
+ <!-- Right: 2×1 grid cells (spreadsheet) -->
16
+ <rect x="64" y="36" width="36" height="24" rx="5" fill="#fff" opacity="0.92"/>
17
+ <rect x="64" y="66" width="36" height="24" rx="5" fill="#fff" opacity="0.68"/>
18
+
19
+ <!-- Bridge: connector -->
20
+ <line x1="55" y1="64" x2="61" y2="64" stroke="#fff" stroke-width="4" stroke-linecap="round" opacity="0.60"/>
21
+ </svg>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlalchemy-excel"
7
- version = "0.2.2"
7
+ version = "0.3.1"
8
8
  description = "SQLAlchemy dialect for Excel files — use Excel as a database"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -33,6 +33,7 @@ dependencies = [
33
33
  ]
34
34
 
35
35
  [project.optional-dependencies]
36
+ graph = ["excel-dbapi[graph]>=0.2.0"]
36
37
  dev = [
37
38
  "pytest>=8.0",
38
39
  "pytest-cov>=4.0",
@@ -42,6 +43,7 @@ dev = [
42
43
 
43
44
  [project.entry-points."sqlalchemy.dialects"]
44
45
  excel = "sqlalchemy_excel.dialect:ExcelDialect"
46
+ "excel.graph" = "sqlalchemy_excel.dialect:ExcelGraphDialect"
45
47
 
46
48
  [project.urls]
47
49
  Homepage = "https://github.com/yeongseon/sqlalchemy-excel"
@@ -2,11 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from .dialect import ExcelDialect
5
+ from .dialect import ExcelDialect, ExcelGraphDialect
6
6
 
7
- __version__ = "0.2.2"
7
+ __version__ = "0.3.1"
8
8
 
9
9
  __all__ = [
10
10
  "ExcelDialect",
11
+ "ExcelGraphDialect",
11
12
  "__version__",
12
13
  ]
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import re
6
6
  from typing import TYPE_CHECKING, Any, Literal, cast
7
+ from urllib.parse import unquote as _url_unquote
7
8
 
8
9
  from sqlalchemy import event, pool
9
10
  from sqlalchemy.engine import default
@@ -230,3 +231,63 @@ class ExcelDialect( # type: ignore[misc] # pyright: ignore[reportIncompatibleM
230
231
  def do_close(self, dbapi_connection: Any) -> None:
231
232
  """Close the underlying excel-dbapi connection."""
232
233
  dbapi_connection.close()
234
+
235
+
236
+ class ExcelGraphDialect(ExcelDialect): # type: ignore[misc,unused-ignore]
237
+ """SQLAlchemy dialect for remote Excel files via Microsoft Graph API.
238
+
239
+ Connection URLs::
240
+
241
+ # With drive_id and item_id
242
+ excel+graph:///drive_id/item_id
243
+
244
+ # With query parameters
245
+ excel+graph:///drive_id/item_id?readonly=false
246
+
247
+ Credentials must be passed via ``connect_args``::
248
+
249
+ engine = create_engine(
250
+ "excel+graph:///drive_id/item_id",
251
+ connect_args={"credential": DefaultAzureCredential()},
252
+ )
253
+ """
254
+
255
+ driver: str = "graph"
256
+ supports_statement_cache: bool = False
257
+
258
+ def create_connect_args(self, url: URL) -> ConnectArgsType:
259
+ """Translate an excel+graph:// URL to excel-dbapi connect() arguments.
260
+
261
+ URL format: excel+graph:///drive_id/item_id
262
+ Maps to DSN: msgraph://drives/{drive_id}/items/{item_id}
263
+ """
264
+ database = url.database
265
+ if not database:
266
+ raise ValueError(
267
+ "No drive/item path in URL. Use excel+graph:///drive_id/item_id"
268
+ )
269
+
270
+ parts = database.strip("/").split("/")
271
+ if len(parts) != 2:
272
+ raise ValueError(
273
+ f"Expected excel+graph:///drive_id/item_id (got {len(parts)} path segments: {database!r})"
274
+ )
275
+
276
+ drive_id = _url_unquote(parts[0])
277
+ item_id = _url_unquote(parts[1])
278
+ dsn = f"msgraph://drives/{drive_id}/items/{item_id}"
279
+
280
+ kwargs = {
281
+ "file_path": dsn,
282
+ "engine": "graph",
283
+ "autocommit": True,
284
+ "create": False,
285
+ }
286
+
287
+ query = dict(url.query)
288
+ if "readonly" in query:
289
+ raw = query.pop("readonly")
290
+ val = raw[0] if isinstance(raw, tuple) else raw
291
+ kwargs["readonly"] = str(val).lower() in ("true", "1", "yes")
292
+
293
+ return ([], kwargs)
@@ -0,0 +1,170 @@
1
+ """Tests for ExcelGraphDialect — URL parsing and Graph API integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+ import pytest
7
+ from sqlalchemy import create_engine, text
8
+ from sqlalchemy.dialects import registry
9
+ from sqlalchemy.engine import make_url
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Mock transport (minimal Graph API stub)
13
+ # ---------------------------------------------------------------------------
14
+
15
+
16
+ def _graph_handler(request: httpx.Request) -> httpx.Response:
17
+ """Stateless mock handler for read-only Graph API tests."""
18
+ path = request.url.path
19
+ method = request.method
20
+
21
+ if path.endswith("/createSession"):
22
+ return httpx.Response(201, json={"id": "sess-graph-test"})
23
+ if path.endswith("/closeSession"):
24
+ return httpx.Response(204)
25
+
26
+ if (
27
+ path.endswith("/worksheets") or "/worksheets?" in str(request.url)
28
+ ) and method == "GET":
29
+ return httpx.Response(
30
+ 200,
31
+ json={"value": [{"id": "ws-sheet1", "name": "Sheet1"}]},
32
+ )
33
+
34
+ if "usedRange" in path and method == "GET":
35
+ return httpx.Response(
36
+ 200,
37
+ json={
38
+ "values": [
39
+ ["id", "name", "value"],
40
+ [1, "Alice", 100],
41
+ [2, "Bob", 200],
42
+ ]
43
+ },
44
+ )
45
+
46
+ return httpx.Response(404)
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # URL Parsing Tests
51
+ # ---------------------------------------------------------------------------
52
+
53
+
54
+ class TestGraphURLParsing:
55
+ def test_url_components(self):
56
+ url = make_url("excel+graph:///drv-abc/itm-xyz")
57
+ assert url.get_backend_name() == "excel"
58
+ assert url.get_driver_name() == "graph"
59
+ assert url.database == "drv-abc/itm-xyz"
60
+
61
+ def test_url_with_host_ignored(self):
62
+ """Host part (tenant_id) is allowed but unused."""
63
+ url = make_url("excel+graph://my-tenant/drv-abc/itm-xyz")
64
+ assert url.host == "my-tenant"
65
+ assert url.database == "drv-abc/itm-xyz"
66
+
67
+ def test_create_connect_args_basic(self):
68
+ dialect = registry.load("excel.graph")()
69
+ url = make_url("excel+graph:///drv-abc/itm-xyz")
70
+ args, kwargs = dialect.create_connect_args(url)
71
+ assert args == []
72
+ assert kwargs["file_path"] == "msgraph://drives/drv-abc/items/itm-xyz"
73
+ assert kwargs["engine"] == "graph"
74
+ assert kwargs["autocommit"] is True
75
+ assert kwargs["create"] is False
76
+
77
+ def test_create_connect_args_url_decoding(self):
78
+ """Drive/item IDs with percent-encoded chars should be decoded."""
79
+ dialect = registry.load("excel.graph")()
80
+ url = make_url("excel+graph:///b%21abc/itm%2D123")
81
+ _, kwargs = dialect.create_connect_args(url)
82
+ assert kwargs["file_path"] == "msgraph://drives/b!abc/items/itm-123"
83
+
84
+ def test_create_connect_args_readonly_false(self):
85
+ dialect = registry.load("excel.graph")()
86
+ url = make_url("excel+graph:///drv/itm?readonly=false")
87
+ _, kwargs = dialect.create_connect_args(url)
88
+ assert kwargs.get("readonly") is False
89
+
90
+ def test_create_connect_args_readonly_true(self):
91
+ dialect = registry.load("excel.graph")()
92
+ url = make_url("excel+graph:///drv/itm?readonly=true")
93
+ _, kwargs = dialect.create_connect_args(url)
94
+ assert kwargs.get("readonly") is True
95
+
96
+ def test_empty_path_raises(self):
97
+ dialect = registry.load("excel.graph")()
98
+ url = make_url("excel+graph://")
99
+ with pytest.raises(ValueError, match="No drive/item path"):
100
+ _ = dialect.create_connect_args(url)
101
+
102
+ def test_single_segment_raises(self):
103
+ dialect = registry.load("excel.graph")()
104
+ url = make_url("excel+graph:///only-one")
105
+ with pytest.raises(ValueError, match="path segments"):
106
+ _ = dialect.create_connect_args(url)
107
+
108
+ def test_three_segments_raises(self):
109
+ dialect = registry.load("excel.graph")()
110
+ url = make_url("excel+graph:///a/b/c")
111
+ with pytest.raises(ValueError, match="path segments"):
112
+ _ = dialect.create_connect_args(url)
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # Dialect Feature Flags
117
+ # ---------------------------------------------------------------------------
118
+
119
+
120
+ class TestGraphDialectFlags:
121
+ def test_driver(self):
122
+ d = registry.load("excel.graph")()
123
+ assert d.driver == "graph"
124
+
125
+ def test_name(self):
126
+ d = registry.load("excel.graph")()
127
+ assert d.name == "excel"
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Integration: SELECT via mock transport
132
+ # ---------------------------------------------------------------------------
133
+
134
+
135
+ class TestGraphDialectIntegration:
136
+ def test_select_via_engine(self):
137
+ """Full round-trip: create_engine → connect → SELECT."""
138
+ transport = httpx.MockTransport(_graph_handler)
139
+ engine = create_engine(
140
+ "excel+graph:///drv-test/itm-test",
141
+ connect_args={
142
+ "credential": "test-token",
143
+ "transport": transport,
144
+ },
145
+ )
146
+ with engine.connect() as conn:
147
+ result = conn.execute(text("SELECT * FROM Sheet1"))
148
+ rows = result.fetchall()
149
+ assert len(rows) == 2
150
+ assert rows[0] == (1, "Alice", 100)
151
+ engine.dispose()
152
+
153
+ def test_select_with_where(self):
154
+ transport = httpx.MockTransport(_graph_handler)
155
+ engine = create_engine(
156
+ "excel+graph:///drv-test/itm-test",
157
+ connect_args={
158
+ "credential": "test-token",
159
+ "transport": transport,
160
+ },
161
+ )
162
+ with engine.connect() as conn:
163
+ result = conn.execute(
164
+ text("SELECT name FROM Sheet1 WHERE id = :id"),
165
+ {"id": 1},
166
+ )
167
+ rows = result.fetchall()
168
+ assert len(rows) == 1
169
+ assert rows[0] == ("Alice",)
170
+ engine.dispose()
@@ -1,13 +0,0 @@
1
- # Changelog
2
-
3
- ## 0.1.0 (2026-04-12)
4
-
5
- - Initial release
6
- - SQLAlchemy 2.0 dialect for Excel files
7
- - PEP 249 DB-API 2.0 driver via excel-dbapi
8
- - SQL support: SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE
9
- - WHERE clause with AND/OR, comparison operators, IS NULL, IS NOT NULL
10
- - ORDER BY, LIMIT
11
- - Type mapping: TEXT, INTEGER, FLOAT, BOOLEAN, DATE, DATETIME
12
- - Reflection: get_table_names, get_columns, get_pk_constraint, has_table
13
- - ORM support with DeclarativeBase
@@ -1,23 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
2
- <!-- SQL code bracket left -->
3
- <path d="M10 38 L22 28 L22 34 L16 40 L16 64 L22 70 L22 76 L10 66 L10 38Z" fill="#CE4926" opacity="0.85" />
4
- <!-- SQL code bracket right -->
5
- <path d="M58 38 L46 28 L46 34 L52 40 L52 64 L46 70 L46 76 L58 66 L58 38Z" fill="#CE4926" opacity="0.85" />
6
- <!-- SQL text -->
7
- <text x="34" y="58" font-family="monospace" font-size="14" font-weight="bold" fill="#CE4926" text-anchor="middle">SA</text>
8
- <!-- Arrow / bridge -->
9
- <line x1="62" y1="52" x2="72" y2="52" stroke="#666" stroke-width="2" stroke-linecap="round" />
10
- <polygon points="72,48 80,52 72,56" fill="#666" />
11
- <!-- Excel spreadsheet -->
12
- <rect x="82" y="16" width="40" height="56" rx="4" fill="#217346" />
13
- <rect x="86" y="22" width="32" height="44" rx="2" fill="#fff" />
14
- <!-- Grid lines -->
15
- <line x1="86" y1="33" x2="118" y2="33" stroke="#217346" stroke-width="1" opacity="0.3" />
16
- <line x1="86" y1="44" x2="118" y2="44" stroke="#217346" stroke-width="1" opacity="0.3" />
17
- <line x1="86" y1="55" x2="118" y2="55" stroke="#217346" stroke-width="1" opacity="0.3" />
18
- <line x1="97" y1="22" x2="97" y2="66" stroke="#217346" stroke-width="1" opacity="0.3" />
19
- <line x1="107" y1="22" x2="107" y2="66" stroke="#217346" stroke-width="1" opacity="0.3" />
20
- <!-- Dialect label -->
21
- <rect x="14" y="88" width="100" height="24" rx="12" fill="#3572A5" />
22
- <text x="64" y="104" font-family="sans-serif" font-size="11" font-weight="bold" fill="#fff" text-anchor="middle">dialect</text>
23
- </svg>