sqlalchemy-excel 0.1.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.
- sqlalchemy_excel-0.1.1/.github/workflows/ci.yml +39 -0
- sqlalchemy_excel-0.1.1/.github/workflows/publish-pypi.yml +28 -0
- sqlalchemy_excel-0.1.1/.gitignore +37 -0
- sqlalchemy_excel-0.1.1/CHANGELOG.md +13 -0
- sqlalchemy_excel-0.1.1/LICENSE +21 -0
- sqlalchemy_excel-0.1.1/PKG-INFO +96 -0
- sqlalchemy_excel-0.1.1/README.md +64 -0
- sqlalchemy_excel-0.1.1/pyproject.toml +106 -0
- sqlalchemy_excel-0.1.1/src/sqlalchemy_excel/__init__.py +12 -0
- sqlalchemy_excel-0.1.1/src/sqlalchemy_excel/compiler.py +159 -0
- sqlalchemy_excel-0.1.1/src/sqlalchemy_excel/ddl.py +57 -0
- sqlalchemy_excel-0.1.1/src/sqlalchemy_excel/dialect.py +221 -0
- sqlalchemy_excel-0.1.1/src/sqlalchemy_excel/reflection.py +173 -0
- sqlalchemy_excel-0.1.1/src/sqlalchemy_excel/types.py +100 -0
- sqlalchemy_excel-0.1.1/tests/conftest.py +28 -0
- sqlalchemy_excel-0.1.1/tests/test_compiler.py +107 -0
- sqlalchemy_excel-0.1.1/tests/test_ddl.py +81 -0
- sqlalchemy_excel-0.1.1/tests/test_dialect.py +105 -0
- sqlalchemy_excel-0.1.1/tests/test_dml.py +184 -0
- sqlalchemy_excel-0.1.1/tests/test_orm.py +127 -0
- sqlalchemy_excel-0.1.1/tests/test_reflection.py +127 -0
- sqlalchemy_excel-0.1.1/tests/test_types.py +68 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: |
|
|
26
|
+
pip install -e ".[dev]"
|
|
27
|
+
|
|
28
|
+
- name: Lint
|
|
29
|
+
run: |
|
|
30
|
+
ruff check .
|
|
31
|
+
ruff format --check .
|
|
32
|
+
|
|
33
|
+
- name: Type check
|
|
34
|
+
run: |
|
|
35
|
+
mypy --strict src/
|
|
36
|
+
|
|
37
|
+
- name: Test
|
|
38
|
+
run: |
|
|
39
|
+
pytest -v --tb=short
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Install build tools
|
|
22
|
+
run: pip install build
|
|
23
|
+
|
|
24
|
+
- name: Build package
|
|
25
|
+
run: python -m build
|
|
26
|
+
|
|
27
|
+
- name: Publish to PyPI
|
|
28
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
|
|
17
|
+
# IDE
|
|
18
|
+
.idea/
|
|
19
|
+
.vscode/
|
|
20
|
+
*.swp
|
|
21
|
+
*.swo
|
|
22
|
+
|
|
23
|
+
# Testing / coverage
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
.coverage
|
|
26
|
+
htmlcov/
|
|
27
|
+
.mypy_cache/
|
|
28
|
+
.ruff_cache/
|
|
29
|
+
.benchmarks/
|
|
30
|
+
|
|
31
|
+
# OS
|
|
32
|
+
.DS_Store
|
|
33
|
+
Thumbs.db
|
|
34
|
+
|
|
35
|
+
# Test artifacts
|
|
36
|
+
*.xlsx
|
|
37
|
+
!tests/fixtures/*.xlsx
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Yeongseon Choe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlalchemy-excel
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: SQLAlchemy dialect for Excel files — use Excel as a database
|
|
5
|
+
Project-URL: Homepage, https://github.com/yeongseon/sqlalchemy-excel
|
|
6
|
+
Project-URL: Repository, https://github.com/yeongseon/sqlalchemy-excel
|
|
7
|
+
Project-URL: Issues, https://github.com/yeongseon/sqlalchemy-excel/issues
|
|
8
|
+
Author: Yeongseon Choe
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: database,dialect,excel,openpyxl,sqlalchemy
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Database
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: excel-dbapi>=0.1.1
|
|
25
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# sqlalchemy-excel
|
|
34
|
+
|
|
35
|
+
SQLAlchemy dialect for Excel files — use Excel as a database.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from sqlalchemy import create_engine, Column, Integer, String
|
|
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__ = "Sheet1"
|
|
48
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
49
|
+
name: Mapped[str] = mapped_column()
|
|
50
|
+
|
|
51
|
+
Base.metadata.create_all(engine)
|
|
52
|
+
|
|
53
|
+
with Session(engine) as session:
|
|
54
|
+
session.add(User(id=1, name="Alice"))
|
|
55
|
+
session.commit()
|
|
56
|
+
|
|
57
|
+
with Session(engine) as session:
|
|
58
|
+
users = session.query(User).all()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install sqlalchemy-excel
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## URL Format
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# Relative path
|
|
71
|
+
engine = create_engine("excel:///data.xlsx")
|
|
72
|
+
|
|
73
|
+
# Absolute path (note four slashes)
|
|
74
|
+
engine = create_engine("excel:////home/user/data.xlsx")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Features
|
|
78
|
+
|
|
79
|
+
- Full SQLAlchemy 2.0 dialect
|
|
80
|
+
- PEP 249 DB-API 2.0 compliant driver ([excel-dbapi](https://github.com/yeongseon/excel-dbapi))
|
|
81
|
+
- SELECT with WHERE, ORDER BY, LIMIT
|
|
82
|
+
- INSERT, UPDATE, DELETE
|
|
83
|
+
- CREATE TABLE / DROP TABLE
|
|
84
|
+
- ORM support with `DeclarativeBase`
|
|
85
|
+
- Type mapping: String, Integer, Float, Boolean, Date, DateTime
|
|
86
|
+
|
|
87
|
+
## Limitations
|
|
88
|
+
|
|
89
|
+
- No JOIN, GROUP BY, HAVING, DISTINCT, OFFSET
|
|
90
|
+
- No subqueries, CTEs, or aggregate functions
|
|
91
|
+
- No ALTER TABLE, foreign keys, or indexes
|
|
92
|
+
- Single-table operations only
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# sqlalchemy-excel
|
|
2
|
+
|
|
3
|
+
SQLAlchemy dialect for Excel files — use Excel as a database.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from sqlalchemy import create_engine, Column, Integer, String
|
|
7
|
+
from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column
|
|
8
|
+
|
|
9
|
+
engine = create_engine("excel:///data.xlsx")
|
|
10
|
+
|
|
11
|
+
class Base(DeclarativeBase):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
class User(Base):
|
|
15
|
+
__tablename__ = "Sheet1"
|
|
16
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
17
|
+
name: Mapped[str] = mapped_column()
|
|
18
|
+
|
|
19
|
+
Base.metadata.create_all(engine)
|
|
20
|
+
|
|
21
|
+
with Session(engine) as session:
|
|
22
|
+
session.add(User(id=1, name="Alice"))
|
|
23
|
+
session.commit()
|
|
24
|
+
|
|
25
|
+
with Session(engine) as session:
|
|
26
|
+
users = session.query(User).all()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install sqlalchemy-excel
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## URL Format
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
# Relative path
|
|
39
|
+
engine = create_engine("excel:///data.xlsx")
|
|
40
|
+
|
|
41
|
+
# Absolute path (note four slashes)
|
|
42
|
+
engine = create_engine("excel:////home/user/data.xlsx")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- Full SQLAlchemy 2.0 dialect
|
|
48
|
+
- PEP 249 DB-API 2.0 compliant driver ([excel-dbapi](https://github.com/yeongseon/excel-dbapi))
|
|
49
|
+
- SELECT with WHERE, ORDER BY, LIMIT
|
|
50
|
+
- INSERT, UPDATE, DELETE
|
|
51
|
+
- CREATE TABLE / DROP TABLE
|
|
52
|
+
- ORM support with `DeclarativeBase`
|
|
53
|
+
- Type mapping: String, Integer, Float, Boolean, Date, DateTime
|
|
54
|
+
|
|
55
|
+
## Limitations
|
|
56
|
+
|
|
57
|
+
- No JOIN, GROUP BY, HAVING, DISTINCT, OFFSET
|
|
58
|
+
- No subqueries, CTEs, or aggregate functions
|
|
59
|
+
- No ALTER TABLE, foreign keys, or indexes
|
|
60
|
+
- Single-table operations only
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sqlalchemy-excel"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "SQLAlchemy dialect for Excel files — use Excel as a database"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Yeongseon Choe"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["sqlalchemy", "excel", "dialect", "database", "openpyxl"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Database",
|
|
26
|
+
"Topic :: Office/Business :: Financial :: Spreadsheet",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
dependencies = [
|
|
31
|
+
"sqlalchemy>=2.0",
|
|
32
|
+
"excel-dbapi>=0.1.1",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=8.0",
|
|
38
|
+
"pytest-cov>=4.0",
|
|
39
|
+
"ruff>=0.4",
|
|
40
|
+
"mypy>=1.10",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.entry-points."sqlalchemy.dialects"]
|
|
44
|
+
excel = "sqlalchemy_excel.dialect:ExcelDialect"
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/yeongseon/sqlalchemy-excel"
|
|
48
|
+
Repository = "https://github.com/yeongseon/sqlalchemy-excel"
|
|
49
|
+
Issues = "https://github.com/yeongseon/sqlalchemy-excel/issues"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/sqlalchemy_excel"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
target-version = "py310"
|
|
56
|
+
src = ["src"]
|
|
57
|
+
line-length = 88
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint]
|
|
60
|
+
select = [
|
|
61
|
+
"E", # pycodestyle errors
|
|
62
|
+
"W", # pycodestyle warnings
|
|
63
|
+
"F", # pyflakes
|
|
64
|
+
"I", # isort
|
|
65
|
+
"N", # pep8-naming
|
|
66
|
+
"UP", # pyupgrade
|
|
67
|
+
"B", # flake8-bugbear
|
|
68
|
+
"SIM", # flake8-simplify
|
|
69
|
+
"TCH", # flake8-type-checking
|
|
70
|
+
"RUF", # ruff-specific
|
|
71
|
+
]
|
|
72
|
+
ignore = ["E501"]
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint.per-file-ignores]
|
|
75
|
+
"src/sqlalchemy_excel/types.py" = ["N802"] # SQLAlchemy visit_TYPE convention
|
|
76
|
+
"src/sqlalchemy_excel/compiler.py" = ["RUF005"] # Match SQLAlchemy's tuple concat pattern
|
|
77
|
+
|
|
78
|
+
[tool.ruff.lint.isort]
|
|
79
|
+
known-first-party = ["sqlalchemy_excel"]
|
|
80
|
+
|
|
81
|
+
[tool.mypy]
|
|
82
|
+
python_version = "3.10"
|
|
83
|
+
strict = true
|
|
84
|
+
warn_return_any = true
|
|
85
|
+
warn_unused_configs = true
|
|
86
|
+
|
|
87
|
+
[[tool.mypy.overrides]]
|
|
88
|
+
module = "excel_dbapi.*"
|
|
89
|
+
ignore_missing_imports = true
|
|
90
|
+
|
|
91
|
+
[tool.pytest.ini_options]
|
|
92
|
+
testpaths = ["tests"]
|
|
93
|
+
addopts = "-ra -q"
|
|
94
|
+
pythonpath = ["src"]
|
|
95
|
+
|
|
96
|
+
[tool.coverage.run]
|
|
97
|
+
source = ["sqlalchemy_excel"]
|
|
98
|
+
branch = true
|
|
99
|
+
|
|
100
|
+
[tool.coverage.report]
|
|
101
|
+
exclude_lines = [
|
|
102
|
+
"pragma: no cover",
|
|
103
|
+
"if TYPE_CHECKING:",
|
|
104
|
+
"if __name__",
|
|
105
|
+
"@overload",
|
|
106
|
+
]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""SQL compiler for the Excel dialect.
|
|
2
|
+
|
|
3
|
+
Compiles SQLAlchemy expression trees into the SQL subset that
|
|
4
|
+
excel-dbapi's parser understands:
|
|
5
|
+
|
|
6
|
+
Supported:
|
|
7
|
+
SELECT columns FROM table [WHERE ...] [ORDER BY col [ASC|DESC]] [LIMIT n]
|
|
8
|
+
INSERT INTO table (cols) VALUES (vals)
|
|
9
|
+
UPDATE table SET col=val [WHERE ...]
|
|
10
|
+
DELETE FROM table [WHERE ...]
|
|
11
|
+
|
|
12
|
+
Rejected (raises CompileError):
|
|
13
|
+
JOIN, GROUP BY, HAVING, DISTINCT, OFFSET, subqueries,
|
|
14
|
+
CTEs, aggregate functions, window functions, RETURNING
|
|
15
|
+
|
|
16
|
+
excel-dbapi's parser uses unquoted, unprefixed column names:
|
|
17
|
+
SELECT id, name FROM users (correct)
|
|
18
|
+
SELECT users.id, users.name FROM users (WRONG — parser rejects)
|
|
19
|
+
|
|
20
|
+
So we override the identifier preparer to never use table prefixes.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Any, ClassVar
|
|
26
|
+
|
|
27
|
+
from sqlalchemy import exc
|
|
28
|
+
from sqlalchemy.sql import compiler, elements
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ExcelIdentifierPreparer(compiler.IdentifierPreparer):
|
|
32
|
+
"""Identifier preparer that never quotes identifiers.
|
|
33
|
+
|
|
34
|
+
excel-dbapi's parser expects bare column names without quotes
|
|
35
|
+
or table prefixes.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
reserved_words: ClassVar[set[str]] = set()
|
|
39
|
+
|
|
40
|
+
def __init__(self, dialect: Any) -> None:
|
|
41
|
+
super().__init__(dialect, initial_quote="", final_quote="")
|
|
42
|
+
|
|
43
|
+
def quote_identifier(self, value: str) -> str:
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
def quote(self, ident: str, force: Any = None) -> str:
|
|
47
|
+
return ident
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ExcelCompiler(compiler.SQLCompiler):
|
|
51
|
+
"""Compiles SQLAlchemy SQL expressions for excel-dbapi."""
|
|
52
|
+
|
|
53
|
+
def visit_column(
|
|
54
|
+
self,
|
|
55
|
+
column: Any,
|
|
56
|
+
add_to_result_map: Any = None,
|
|
57
|
+
include_table: bool = True,
|
|
58
|
+
**kw: Any,
|
|
59
|
+
) -> str:
|
|
60
|
+
"""Override to never include table prefix in column references.
|
|
61
|
+
|
|
62
|
+
excel-dbapi expects: SELECT id, name FROM users
|
|
63
|
+
Not: SELECT users.id, users.name FROM users
|
|
64
|
+
"""
|
|
65
|
+
# Force include_table=False to avoid table.column notation
|
|
66
|
+
return super().visit_column(
|
|
67
|
+
column,
|
|
68
|
+
add_to_result_map=add_to_result_map,
|
|
69
|
+
include_table=False,
|
|
70
|
+
**kw,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def visit_label(
|
|
74
|
+
self,
|
|
75
|
+
label: Any,
|
|
76
|
+
add_to_result_map: Any = None,
|
|
77
|
+
within_label_clause: bool = False,
|
|
78
|
+
within_columns_clause: bool = False,
|
|
79
|
+
render_label_as_label: Any = None,
|
|
80
|
+
result_map_targets: Any = (),
|
|
81
|
+
**kw: Any,
|
|
82
|
+
) -> str:
|
|
83
|
+
"""Override to never emit AS <label> in SELECT columns.
|
|
84
|
+
|
|
85
|
+
excel-dbapi's parser does not understand column aliases.
|
|
86
|
+
We still register the result map so SQLAlchemy can map
|
|
87
|
+
result columns back to ORM attributes by position.
|
|
88
|
+
"""
|
|
89
|
+
render_label_with_as = within_columns_clause and not within_label_clause
|
|
90
|
+
|
|
91
|
+
if render_label_with_as:
|
|
92
|
+
# Compute the label name for the result map
|
|
93
|
+
if isinstance(label.name, elements._truncated_label):
|
|
94
|
+
labelname = self._truncated_identifier("colident", label.name)
|
|
95
|
+
else:
|
|
96
|
+
labelname = label.name
|
|
97
|
+
|
|
98
|
+
if add_to_result_map is not None:
|
|
99
|
+
add_to_result_map(
|
|
100
|
+
labelname,
|
|
101
|
+
label.name,
|
|
102
|
+
(label, labelname) + label._alt_names + result_map_targets,
|
|
103
|
+
label.type,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Emit the column WITHOUT "AS <label>"
|
|
107
|
+
return label.element._compiler_dispatch(
|
|
108
|
+
self,
|
|
109
|
+
within_columns_clause=True,
|
|
110
|
+
within_label_clause=True,
|
|
111
|
+
**kw,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if render_label_as_label is label:
|
|
115
|
+
if isinstance(label.name, elements._truncated_label):
|
|
116
|
+
labelname = self._truncated_identifier("colident", label.name)
|
|
117
|
+
else:
|
|
118
|
+
labelname = label.name
|
|
119
|
+
return self.preparer.format_label(label, labelname)
|
|
120
|
+
|
|
121
|
+
return label.element._compiler_dispatch(self, within_columns_clause=False, **kw)
|
|
122
|
+
|
|
123
|
+
# ── Unsupported feature guards ─────────────────────────
|
|
124
|
+
|
|
125
|
+
def visit_join(self, join: Any, **kw: Any) -> str:
|
|
126
|
+
raise exc.CompileError("Excel dialect does not support JOIN")
|
|
127
|
+
|
|
128
|
+
def group_by_clause(self, select: Any, **kw: Any) -> str:
|
|
129
|
+
if select._group_by_clauses:
|
|
130
|
+
raise exc.CompileError("Excel dialect does not support GROUP BY")
|
|
131
|
+
return ""
|
|
132
|
+
|
|
133
|
+
def having_clause(self, select: Any, **kw: Any) -> str:
|
|
134
|
+
raise exc.CompileError("Excel dialect does not support HAVING")
|
|
135
|
+
|
|
136
|
+
def limit_clause(self, select: Any, **kw: Any) -> str:
|
|
137
|
+
text = ""
|
|
138
|
+
if select._limit_clause is not None:
|
|
139
|
+
text += " LIMIT " + self.process(select._limit_clause, **kw)
|
|
140
|
+
if select._offset_clause is not None:
|
|
141
|
+
raise exc.CompileError("Excel dialect does not support OFFSET")
|
|
142
|
+
return text
|
|
143
|
+
|
|
144
|
+
def visit_cte(self, cte: Any, **kw: Any) -> str:
|
|
145
|
+
raise exc.CompileError("Excel dialect does not support CTEs")
|
|
146
|
+
|
|
147
|
+
def visit_subquery(self, subquery: Any, **kw: Any) -> str:
|
|
148
|
+
raise exc.CompileError("Excel dialect does not support subqueries")
|
|
149
|
+
|
|
150
|
+
def returning_clause(
|
|
151
|
+
self,
|
|
152
|
+
stmt: Any,
|
|
153
|
+
returning_cols: Any,
|
|
154
|
+
**kw: Any,
|
|
155
|
+
) -> str:
|
|
156
|
+
raise exc.CompileError("Excel dialect does not support RETURNING")
|
|
157
|
+
|
|
158
|
+
def for_update_clause(self, select: Any, **kw: Any) -> str:
|
|
159
|
+
raise exc.CompileError("Excel dialect does not support SELECT ... FOR UPDATE")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""DDL compiler for the Excel dialect.
|
|
2
|
+
|
|
3
|
+
CREATE TABLE → creates a worksheet and writes column metadata.
|
|
4
|
+
DROP TABLE → deletes the worksheet and removes metadata.
|
|
5
|
+
ALTER TABLE → rejected (not supported).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from sqlalchemy import exc
|
|
13
|
+
from sqlalchemy.sql import compiler
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ExcelDDLCompiler(compiler.DDLCompiler):
|
|
17
|
+
"""Compiles DDL statements for excel-dbapi."""
|
|
18
|
+
|
|
19
|
+
def visit_create_table(self, create: Any, **kw: Any) -> str:
|
|
20
|
+
"""Compile CREATE TABLE into SQL that excel-dbapi's parser accepts.
|
|
21
|
+
|
|
22
|
+
Format: CREATE TABLE name (col1 TYPE, col2 TYPE, ...)
|
|
23
|
+
"""
|
|
24
|
+
table = create.element
|
|
25
|
+
table_name = self.preparer.format_table(table)
|
|
26
|
+
|
|
27
|
+
columns = []
|
|
28
|
+
for col in table.columns:
|
|
29
|
+
col_name = self.preparer.format_column(col)
|
|
30
|
+
col_type = self.dialect.type_compiler.process(col.type)
|
|
31
|
+
columns.append(f"{col_name} {col_type}")
|
|
32
|
+
|
|
33
|
+
return f"CREATE TABLE {table_name} ({', '.join(columns)})"
|
|
34
|
+
|
|
35
|
+
def visit_drop_table(self, drop: Any, **kw: Any) -> str:
|
|
36
|
+
"""Compile DROP TABLE."""
|
|
37
|
+
table = drop.element
|
|
38
|
+
table_name = self.preparer.format_table(table)
|
|
39
|
+
return f"DROP TABLE {table_name}"
|
|
40
|
+
|
|
41
|
+
def visit_create_index(self, create: Any, **kw: Any) -> str:
|
|
42
|
+
raise exc.CompileError("Excel dialect does not support CREATE INDEX")
|
|
43
|
+
|
|
44
|
+
def visit_drop_index(self, drop: Any, **kw: Any) -> str:
|
|
45
|
+
raise exc.CompileError("Excel dialect does not support DROP INDEX")
|
|
46
|
+
|
|
47
|
+
def visit_add_constraint(self, create: Any, **kw: Any) -> str:
|
|
48
|
+
raise exc.CompileError("Excel dialect does not support constraints")
|
|
49
|
+
|
|
50
|
+
def visit_drop_constraint(self, drop: Any, **kw: Any) -> str:
|
|
51
|
+
raise exc.CompileError("Excel dialect does not support constraints")
|
|
52
|
+
|
|
53
|
+
def visit_create_sequence(self, create: Any, **kw: Any) -> str:
|
|
54
|
+
raise exc.CompileError("Excel dialect does not support sequences")
|
|
55
|
+
|
|
56
|
+
def visit_drop_sequence(self, drop: Any, **kw: Any) -> str:
|
|
57
|
+
raise exc.CompileError("Excel dialect does not support sequences")
|