sqlalchemy-excel 0.1.1__tar.gz → 0.2.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.
Files changed (24) hide show
  1. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/.github/workflows/ci.yml +9 -2
  2. sqlalchemy_excel-0.2.0/PKG-INFO +236 -0
  3. sqlalchemy_excel-0.2.0/README.md +204 -0
  4. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/pyproject.toml +2 -2
  5. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/src/sqlalchemy_excel/__init__.py +1 -1
  6. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/src/sqlalchemy_excel/compiler.py +44 -14
  7. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/src/sqlalchemy_excel/ddl.py +13 -2
  8. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/src/sqlalchemy_excel/dialect.py +33 -22
  9. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/src/sqlalchemy_excel/reflection.py +4 -4
  10. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/test_dml.py +55 -0
  11. sqlalchemy_excel-0.1.1/PKG-INFO +0 -96
  12. sqlalchemy_excel-0.1.1/README.md +0 -64
  13. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/.github/workflows/publish-pypi.yml +0 -0
  14. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/.gitignore +0 -0
  15. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/CHANGELOG.md +0 -0
  16. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/LICENSE +0 -0
  17. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/src/sqlalchemy_excel/types.py +0 -0
  18. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/conftest.py +0 -0
  19. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/test_compiler.py +0 -0
  20. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/test_ddl.py +0 -0
  21. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/test_dialect.py +0 -0
  22. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/test_orm.py +0 -0
  23. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/test_reflection.py +0 -0
  24. {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/test_types.py +0 -0
@@ -34,6 +34,13 @@ jobs:
34
34
  run: |
35
35
  mypy --strict src/
36
36
 
37
- - name: Test
37
+ - name: Run tests with coverage
38
38
  run: |
39
- pytest -v --tb=short
39
+ pytest tests/ -v --cov=sqlalchemy_excel --cov-report=xml --cov-report=term-missing
40
+
41
+ - name: Upload coverage to Codecov
42
+ if: matrix.python-version == '3.12'
43
+ uses: codecov/codecov-action@v5
44
+ with:
45
+ files: ./coverage.xml
46
+ fail_ci_if_error: false
@@ -0,0 +1,236 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlalchemy-excel
3
+ Version: 0.2.0
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.2.0
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
+ ![CI](https://github.com/yeongseon/sqlalchemy-excel/actions/workflows/ci.yml/badge.svg)
36
+ [![codecov](https://codecov.io/gh/yeongseon/sqlalchemy-excel/branch/main/graph/badge.svg)](https://codecov.io/gh/yeongseon/sqlalchemy-excel)
37
+ [![PyPI](https://img.shields.io/pypi/v/sqlalchemy-excel.svg)](https://pypi.org/project/sqlalchemy-excel/)
38
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
40
+
41
+ SQLAlchemy dialect for Excel files — use Excel as a database.
42
+
43
+ ```python
44
+ from sqlalchemy import create_engine, Column, Integer, String
45
+ from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column
46
+
47
+ engine = create_engine("excel:///data.xlsx")
48
+
49
+ class Base(DeclarativeBase):
50
+ pass
51
+
52
+ class User(Base):
53
+ __tablename__ = "Sheet1"
54
+ id: Mapped[int] = mapped_column(primary_key=True)
55
+ name: Mapped[str] = mapped_column()
56
+
57
+ Base.metadata.create_all(engine)
58
+
59
+ with Session(engine) as session:
60
+ session.add(User(id=1, name="Alice"))
61
+ session.commit()
62
+
63
+ with Session(engine) as session:
64
+ users = session.query(User).all()
65
+ ```
66
+
67
+ ## Installation
68
+
69
+ ```bash
70
+ pip install sqlalchemy-excel
71
+ ```
72
+
73
+ `excel-dbapi` is automatically installed as a dependency.
74
+
75
+ ## URL Format
76
+
77
+ ```python
78
+ # Relative path
79
+ engine = create_engine("excel:///data.xlsx")
80
+
81
+ # Absolute path (note four slashes)
82
+ engine = create_engine("excel:////home/user/data.xlsx")
83
+
84
+ # With engine options
85
+ engine = create_engine("excel:///data.xlsx", connect_args={"engine": "openpyxl"})
86
+ ```
87
+
88
+ ## Features
89
+
90
+ - Full SQLAlchemy 2.0 dialect
91
+ - PEP 249 DB-API 2.0 compliant driver ([excel-dbapi](https://github.com/yeongseon/excel-dbapi))
92
+ - SELECT with WHERE, ORDER BY, LIMIT
93
+ - INSERT, UPDATE, DELETE
94
+ - CREATE TABLE / DROP TABLE with metadata tracking
95
+ - IN, BETWEEN, LIKE operators in WHERE clauses
96
+ - ORM support with `DeclarativeBase`
97
+ - Schema inspection (`get_table_names`, `get_columns`, `has_table`)
98
+ - Type mapping: String, Integer, Float, Boolean, Date, DateTime
99
+
100
+ ## Type Mapping
101
+
102
+ | SQLAlchemy Type | Excel Storage | Notes |
103
+ |---|---|---|
104
+ | `String`, `Text`, `VARCHAR`, `CHAR` | TEXT | All string types map to TEXT |
105
+ | `Integer`, `SmallInteger`, `BigInteger` | INTEGER | All integer types map to INTEGER |
106
+ | `Float`, `Numeric`, `Decimal` | FLOAT | All numeric types map to FLOAT |
107
+ | `Boolean` | BOOLEAN | |
108
+ | `Date` | DATE | |
109
+ | `DateTime`, `TIMESTAMP` | DATETIME | |
110
+ | `Time` | TEXT | Stored as text |
111
+ | `Uuid` | TEXT | Stored as text |
112
+
113
+ > BLOB, BINARY, JSON, and ARRAY types are not supported and will raise `CompileError`.
114
+
115
+ ## ORM Examples
116
+
117
+ ### Define a Model
118
+
119
+ ```python
120
+ from sqlalchemy import create_engine
121
+ from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column
122
+
123
+ engine = create_engine("excel:///data.xlsx")
124
+
125
+ class Base(DeclarativeBase):
126
+ pass
127
+
128
+ class User(Base):
129
+ __tablename__ = "users"
130
+ id: Mapped[int] = mapped_column(primary_key=True)
131
+ name: Mapped[str] = mapped_column()
132
+ age: Mapped[int] = mapped_column()
133
+
134
+ Base.metadata.create_all(engine)
135
+ ```
136
+
137
+ ### Insert
138
+
139
+ ```python
140
+ with Session(engine) as session:
141
+ session.add(User(id=1, name="Alice", age=30))
142
+ session.add(User(id=2, name="Bob", age=25))
143
+ session.commit()
144
+ ```
145
+
146
+ ### Query with Filters
147
+
148
+ ```python
149
+ from sqlalchemy import select
150
+
151
+ with Session(engine) as session:
152
+ # Basic query
153
+ users = session.query(User).all()
154
+
155
+ # WHERE clause
156
+ user = session.query(User).filter(User.name == "Alice").first()
157
+
158
+ # IN operator
159
+ stmt = select(User).where(User.name.in_(["Alice", "Bob"]))
160
+ users = session.scalars(stmt).all()
161
+
162
+ # BETWEEN operator
163
+ stmt = select(User).where(User.age.between(25, 35))
164
+ users = session.scalars(stmt).all()
165
+
166
+ # LIKE operator
167
+ stmt = select(User).where(User.name.like("A%"))
168
+ users = session.scalars(stmt).all()
169
+
170
+ # ORDER BY + LIMIT
171
+ stmt = select(User).order_by(User.age.desc()).limit(5)
172
+ users = session.scalars(stmt).all()
173
+ ```
174
+
175
+ ### Update and Delete
176
+
177
+ ```python
178
+ with Session(engine) as session:
179
+ user = session.query(User).filter(User.id == 1).first()
180
+ if user:
181
+ user.name = "Ann"
182
+ session.commit()
183
+
184
+ with Session(engine) as session:
185
+ user = session.query(User).filter(User.id == 2).first()
186
+ if user:
187
+ session.delete(user)
188
+ session.commit()
189
+ ```
190
+
191
+ ## Core Usage
192
+
193
+ ```python
194
+ from sqlalchemy import create_engine, text
195
+
196
+ engine = create_engine("excel:///data.xlsx")
197
+
198
+ with engine.connect() as conn:
199
+ result = conn.execute(text("SELECT * FROM Sheet1"))
200
+ for row in result:
201
+ print(row)
202
+ ```
203
+
204
+ ## Schema Inspection
205
+
206
+ ```python
207
+ from sqlalchemy import create_engine, inspect
208
+
209
+ engine = create_engine("excel:///data.xlsx")
210
+ inspector = inspect(engine)
211
+
212
+ # List all sheets (tables)
213
+ print(inspector.get_table_names())
214
+
215
+ # Get column info
216
+ print(inspector.get_columns("Sheet1"))
217
+
218
+ # Check if a sheet exists
219
+ print(inspector.has_table("Sheet1"))
220
+ ```
221
+
222
+ ## Limitations
223
+
224
+ - No JOIN, GROUP BY, HAVING, DISTINCT, OFFSET
225
+ - No subqueries, CTEs, or aggregate functions
226
+ - No ALTER TABLE, foreign keys, or indexes
227
+ - Single-table operations only
228
+ - No concurrent writes — use a single-writer model
229
+
230
+ ## Related Projects
231
+
232
+ - [excel-dbapi](https://github.com/yeongseon/excel-dbapi) — The underlying PEP 249 DB-API 2.0 driver for Excel files.
233
+
234
+ ## License
235
+
236
+ MIT
@@ -0,0 +1,204 @@
1
+ # sqlalchemy-excel
2
+
3
+ ![CI](https://github.com/yeongseon/sqlalchemy-excel/actions/workflows/ci.yml/badge.svg)
4
+ [![codecov](https://codecov.io/gh/yeongseon/sqlalchemy-excel/branch/main/graph/badge.svg)](https://codecov.io/gh/yeongseon/sqlalchemy-excel)
5
+ [![PyPI](https://img.shields.io/pypi/v/sqlalchemy-excel.svg)](https://pypi.org/project/sqlalchemy-excel/)
6
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ SQLAlchemy dialect for Excel files — use Excel as a database.
10
+
11
+ ```python
12
+ from sqlalchemy import create_engine, Column, Integer, String
13
+ from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column
14
+
15
+ engine = create_engine("excel:///data.xlsx")
16
+
17
+ class Base(DeclarativeBase):
18
+ pass
19
+
20
+ class User(Base):
21
+ __tablename__ = "Sheet1"
22
+ id: Mapped[int] = mapped_column(primary_key=True)
23
+ name: Mapped[str] = mapped_column()
24
+
25
+ Base.metadata.create_all(engine)
26
+
27
+ with Session(engine) as session:
28
+ session.add(User(id=1, name="Alice"))
29
+ session.commit()
30
+
31
+ with Session(engine) as session:
32
+ users = session.query(User).all()
33
+ ```
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install sqlalchemy-excel
39
+ ```
40
+
41
+ `excel-dbapi` is automatically installed as a dependency.
42
+
43
+ ## URL Format
44
+
45
+ ```python
46
+ # Relative path
47
+ engine = create_engine("excel:///data.xlsx")
48
+
49
+ # Absolute path (note four slashes)
50
+ engine = create_engine("excel:////home/user/data.xlsx")
51
+
52
+ # With engine options
53
+ engine = create_engine("excel:///data.xlsx", connect_args={"engine": "openpyxl"})
54
+ ```
55
+
56
+ ## Features
57
+
58
+ - Full SQLAlchemy 2.0 dialect
59
+ - PEP 249 DB-API 2.0 compliant driver ([excel-dbapi](https://github.com/yeongseon/excel-dbapi))
60
+ - SELECT with WHERE, ORDER BY, LIMIT
61
+ - INSERT, UPDATE, DELETE
62
+ - CREATE TABLE / DROP TABLE with metadata tracking
63
+ - IN, BETWEEN, LIKE operators in WHERE clauses
64
+ - ORM support with `DeclarativeBase`
65
+ - Schema inspection (`get_table_names`, `get_columns`, `has_table`)
66
+ - Type mapping: String, Integer, Float, Boolean, Date, DateTime
67
+
68
+ ## Type Mapping
69
+
70
+ | SQLAlchemy Type | Excel Storage | Notes |
71
+ |---|---|---|
72
+ | `String`, `Text`, `VARCHAR`, `CHAR` | TEXT | All string types map to TEXT |
73
+ | `Integer`, `SmallInteger`, `BigInteger` | INTEGER | All integer types map to INTEGER |
74
+ | `Float`, `Numeric`, `Decimal` | FLOAT | All numeric types map to FLOAT |
75
+ | `Boolean` | BOOLEAN | |
76
+ | `Date` | DATE | |
77
+ | `DateTime`, `TIMESTAMP` | DATETIME | |
78
+ | `Time` | TEXT | Stored as text |
79
+ | `Uuid` | TEXT | Stored as text |
80
+
81
+ > BLOB, BINARY, JSON, and ARRAY types are not supported and will raise `CompileError`.
82
+
83
+ ## ORM Examples
84
+
85
+ ### Define a Model
86
+
87
+ ```python
88
+ from sqlalchemy import create_engine
89
+ from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column
90
+
91
+ engine = create_engine("excel:///data.xlsx")
92
+
93
+ class Base(DeclarativeBase):
94
+ pass
95
+
96
+ class User(Base):
97
+ __tablename__ = "users"
98
+ id: Mapped[int] = mapped_column(primary_key=True)
99
+ name: Mapped[str] = mapped_column()
100
+ age: Mapped[int] = mapped_column()
101
+
102
+ Base.metadata.create_all(engine)
103
+ ```
104
+
105
+ ### Insert
106
+
107
+ ```python
108
+ with Session(engine) as session:
109
+ session.add(User(id=1, name="Alice", age=30))
110
+ session.add(User(id=2, name="Bob", age=25))
111
+ session.commit()
112
+ ```
113
+
114
+ ### Query with Filters
115
+
116
+ ```python
117
+ from sqlalchemy import select
118
+
119
+ with Session(engine) as session:
120
+ # Basic query
121
+ users = session.query(User).all()
122
+
123
+ # WHERE clause
124
+ user = session.query(User).filter(User.name == "Alice").first()
125
+
126
+ # IN operator
127
+ stmt = select(User).where(User.name.in_(["Alice", "Bob"]))
128
+ users = session.scalars(stmt).all()
129
+
130
+ # BETWEEN operator
131
+ stmt = select(User).where(User.age.between(25, 35))
132
+ users = session.scalars(stmt).all()
133
+
134
+ # LIKE operator
135
+ stmt = select(User).where(User.name.like("A%"))
136
+ users = session.scalars(stmt).all()
137
+
138
+ # ORDER BY + LIMIT
139
+ stmt = select(User).order_by(User.age.desc()).limit(5)
140
+ users = session.scalars(stmt).all()
141
+ ```
142
+
143
+ ### Update and Delete
144
+
145
+ ```python
146
+ with Session(engine) as session:
147
+ user = session.query(User).filter(User.id == 1).first()
148
+ if user:
149
+ user.name = "Ann"
150
+ session.commit()
151
+
152
+ with Session(engine) as session:
153
+ user = session.query(User).filter(User.id == 2).first()
154
+ if user:
155
+ session.delete(user)
156
+ session.commit()
157
+ ```
158
+
159
+ ## Core Usage
160
+
161
+ ```python
162
+ from sqlalchemy import create_engine, text
163
+
164
+ engine = create_engine("excel:///data.xlsx")
165
+
166
+ with engine.connect() as conn:
167
+ result = conn.execute(text("SELECT * FROM Sheet1"))
168
+ for row in result:
169
+ print(row)
170
+ ```
171
+
172
+ ## Schema Inspection
173
+
174
+ ```python
175
+ from sqlalchemy import create_engine, inspect
176
+
177
+ engine = create_engine("excel:///data.xlsx")
178
+ inspector = inspect(engine)
179
+
180
+ # List all sheets (tables)
181
+ print(inspector.get_table_names())
182
+
183
+ # Get column info
184
+ print(inspector.get_columns("Sheet1"))
185
+
186
+ # Check if a sheet exists
187
+ print(inspector.has_table("Sheet1"))
188
+ ```
189
+
190
+ ## Limitations
191
+
192
+ - No JOIN, GROUP BY, HAVING, DISTINCT, OFFSET
193
+ - No subqueries, CTEs, or aggregate functions
194
+ - No ALTER TABLE, foreign keys, or indexes
195
+ - Single-table operations only
196
+ - No concurrent writes — use a single-writer model
197
+
198
+ ## Related Projects
199
+
200
+ - [excel-dbapi](https://github.com/yeongseon/excel-dbapi) — The underlying PEP 249 DB-API 2.0 driver for Excel files.
201
+
202
+ ## License
203
+
204
+ MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlalchemy-excel"
7
- version = "0.1.1"
7
+ version = "0.2.0"
8
8
  description = "SQLAlchemy dialect for Excel files — use Excel as a database"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -29,7 +29,7 @@ classifiers = [
29
29
 
30
30
  dependencies = [
31
31
  "sqlalchemy>=2.0",
32
- "excel-dbapi>=0.1.1",
32
+ "excel-dbapi>=0.2.0",
33
33
  ]
34
34
 
35
35
  [project.optional-dependencies]
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from .dialect import ExcelDialect
6
6
 
7
- __version__ = "0.1.1"
7
+ __version__ = "0.2.0"
8
8
 
9
9
  __all__ = [
10
10
  "ExcelDialect",
@@ -22,11 +22,14 @@ So we override the identifier preparer to never use table prefixes.
22
22
 
23
23
  from __future__ import annotations
24
24
 
25
- from typing import Any, ClassVar
25
+ from typing import TYPE_CHECKING, Any, Literal
26
26
 
27
27
  from sqlalchemy import exc
28
28
  from sqlalchemy.sql import compiler, elements
29
29
 
30
+ if TYPE_CHECKING:
31
+ from collections.abc import MutableMapping
32
+
30
33
 
31
34
  class ExcelIdentifierPreparer(compiler.IdentifierPreparer):
32
35
  """Identifier preparer that never quotes identifiers.
@@ -35,10 +38,9 @@ class ExcelIdentifierPreparer(compiler.IdentifierPreparer):
35
38
  or table prefixes.
36
39
  """
37
40
 
38
- reserved_words: ClassVar[set[str]] = set()
39
-
40
41
  def __init__(self, dialect: Any) -> None:
41
42
  super().__init__(dialect, initial_quote="", final_quote="")
43
+ self.reserved_words = set()
42
44
 
43
45
  def quote_identifier(self, value: str) -> str:
44
46
  return value
@@ -55,7 +57,9 @@ class ExcelCompiler(compiler.SQLCompiler):
55
57
  column: Any,
56
58
  add_to_result_map: Any = None,
57
59
  include_table: bool = True,
58
- **kw: Any,
60
+ result_map_targets: tuple[Any, ...] = (),
61
+ ambiguous_table_name_map: MutableMapping[str, str] | None = None,
62
+ **kwargs: Any,
59
63
  ) -> str:
60
64
  """Override to never include table prefix in column references.
61
65
 
@@ -67,7 +71,9 @@ class ExcelCompiler(compiler.SQLCompiler):
67
71
  column,
68
72
  add_to_result_map=add_to_result_map,
69
73
  include_table=False,
70
- **kw,
74
+ result_map_targets=result_map_targets,
75
+ ambiguous_table_name_map=ambiguous_table_name_map,
76
+ **kwargs,
71
77
  )
72
78
 
73
79
  def visit_label(
@@ -104,11 +110,13 @@ class ExcelCompiler(compiler.SQLCompiler):
104
110
  )
105
111
 
106
112
  # 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,
113
+ return str(
114
+ label.element._compiler_dispatch(
115
+ self,
116
+ within_columns_clause=True,
117
+ within_label_clause=True,
118
+ **kw,
119
+ )
112
120
  )
113
121
 
114
122
  if render_label_as_label is label:
@@ -118,11 +126,19 @@ class ExcelCompiler(compiler.SQLCompiler):
118
126
  labelname = label.name
119
127
  return self.preparer.format_label(label, labelname)
120
128
 
121
- return label.element._compiler_dispatch(self, within_columns_clause=False, **kw)
129
+ return str(
130
+ label.element._compiler_dispatch(self, within_columns_clause=False, **kw)
131
+ )
122
132
 
123
133
  # ── Unsupported feature guards ─────────────────────────
124
134
 
125
- def visit_join(self, join: Any, **kw: Any) -> str:
135
+ def visit_join(
136
+ self,
137
+ join: Any,
138
+ asfrom: Any = False,
139
+ from_linter: Any = None,
140
+ **kwargs: Any,
141
+ ) -> str:
126
142
  raise exc.CompileError("Excel dialect does not support JOIN")
127
143
 
128
144
  def group_by_clause(self, select: Any, **kw: Any) -> str:
@@ -141,7 +157,17 @@ class ExcelCompiler(compiler.SQLCompiler):
141
157
  raise exc.CompileError("Excel dialect does not support OFFSET")
142
158
  return text
143
159
 
144
- def visit_cte(self, cte: Any, **kw: Any) -> str:
160
+ def visit_cte(
161
+ self,
162
+ cte: Any,
163
+ asfrom: bool = False,
164
+ ashint: bool = False,
165
+ fromhints: dict[Any, str] | None = None,
166
+ visiting_cte: Any = None,
167
+ from_linter: Any = None,
168
+ cte_opts: Any = None,
169
+ **kwargs: Any,
170
+ ) -> str | None:
145
171
  raise exc.CompileError("Excel dialect does not support CTEs")
146
172
 
147
173
  def visit_subquery(self, subquery: Any, **kw: Any) -> str:
@@ -155,5 +181,9 @@ class ExcelCompiler(compiler.SQLCompiler):
155
181
  ) -> str:
156
182
  raise exc.CompileError("Excel dialect does not support RETURNING")
157
183
 
158
- def for_update_clause(self, select: Any, **kw: Any) -> str:
184
+ def for_update_clause(
185
+ self,
186
+ select: Any,
187
+ **kw: Any,
188
+ ) -> Literal[" FOR UPDATE"]:
159
189
  raise exc.CompileError("Excel dialect does not support SELECT ... FOR UPDATE")
@@ -38,7 +38,13 @@ class ExcelDDLCompiler(compiler.DDLCompiler):
38
38
  table_name = self.preparer.format_table(table)
39
39
  return f"DROP TABLE {table_name}"
40
40
 
41
- def visit_create_index(self, create: Any, **kw: Any) -> str:
41
+ def visit_create_index(
42
+ self,
43
+ create: Any,
44
+ include_schema: Any = False,
45
+ include_table_schema: Any = True,
46
+ **kw: Any,
47
+ ) -> str:
42
48
  raise exc.CompileError("Excel dialect does not support CREATE INDEX")
43
49
 
44
50
  def visit_drop_index(self, drop: Any, **kw: Any) -> str:
@@ -50,7 +56,12 @@ class ExcelDDLCompiler(compiler.DDLCompiler):
50
56
  def visit_drop_constraint(self, drop: Any, **kw: Any) -> str:
51
57
  raise exc.CompileError("Excel dialect does not support constraints")
52
58
 
53
- def visit_create_sequence(self, create: Any, **kw: Any) -> str:
59
+ def visit_create_sequence(
60
+ self,
61
+ create: Any,
62
+ prefix: Any = None,
63
+ **kw: Any,
64
+ ) -> str:
54
65
  raise exc.CompileError("Excel dialect does not support sequences")
55
66
 
56
67
  def visit_drop_sequence(self, drop: Any, **kw: Any) -> str:
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import re
6
- from typing import TYPE_CHECKING, Any, ClassVar
6
+ from typing import TYPE_CHECKING, Any, Literal, cast
7
7
 
8
8
  from sqlalchemy import event, pool
9
9
  from sqlalchemy.engine import default
@@ -17,6 +17,7 @@ from .types import ExcelTypeCompiler
17
17
  if TYPE_CHECKING:
18
18
  from sqlalchemy.engine import URL
19
19
  from sqlalchemy.engine.interfaces import ConnectArgsType
20
+ from sqlalchemy.sql.compiler import IdentifierPreparer
20
21
 
21
22
 
22
23
  def _after_create(
@@ -67,7 +68,10 @@ event.listen(Table, "after_create", _after_create)
67
68
  event.listen(Table, "after_drop", _after_drop)
68
69
 
69
70
 
70
- class ExcelDialect(ExcelInspectionMixin, default.DefaultDialect): # type: ignore[misc]
71
+ class ExcelDialect( # type: ignore[misc] # pyright: ignore[reportIncompatibleMethodOverride]
72
+ ExcelInspectionMixin,
73
+ default.DefaultDialect,
74
+ ):
71
75
  """SQLAlchemy dialect for Excel files via excel-dbapi.
72
76
 
73
77
  Connection URLs::
@@ -81,30 +85,32 @@ class ExcelDialect(ExcelInspectionMixin, default.DefaultDialect): # type: ignor
81
85
 
82
86
  """
83
87
 
84
- name: ClassVar[str] = "excel"
85
- driver: ClassVar[str] = "dbapi"
86
- default_paramstyle: ClassVar[str] = "qmark"
88
+ name: str = "excel"
89
+ driver: str = "dbapi"
90
+ default_paramstyle: str = "qmark"
87
91
 
88
92
  # ── Feature flags ──────────────────────────────────────
89
- supports_alter: ClassVar[bool] = False
90
- supports_sequences: ClassVar[bool] = False
91
- supports_schemas: ClassVar[bool] = False
92
- supports_views: ClassVar[bool] = False
93
- supports_native_boolean: ClassVar[bool] = True
94
- supports_native_decimal: ClassVar[bool] = False
95
- supports_statement_cache: ClassVar[bool] = False
96
- supports_default_values: ClassVar[bool] = False
97
- supports_default_metavalue: ClassVar[bool] = False
98
- supports_empty_insert: ClassVar[bool] = False
99
- supports_multivalues_insert: ClassVar[bool] = False
100
- postfetch_lastrowid: ClassVar[bool] = False
101
- insertmanyvalues_implicit_sentinel: ClassVar[Any] = None
93
+ supports_alter: bool = False
94
+ supports_sequences: bool = False
95
+ supports_schemas: bool = False
96
+ supports_views: bool = False
97
+ supports_native_boolean: bool = True
98
+ supports_native_decimal: bool = False
99
+ supports_statement_cache: bool = False
100
+ supports_default_values: bool = False
101
+ supports_default_metavalue: bool = False
102
+ supports_empty_insert: bool = False
103
+ supports_multivalues_insert: bool = False
104
+ postfetch_lastrowid: bool = False
105
+ insertmanyvalues_implicit_sentinel: Any = None
102
106
 
103
107
  # ── Compiler classes ──────────────────────────────────
104
108
  statement_compiler = ExcelCompiler
105
109
  ddl_compiler = ExcelDDLCompiler
106
110
  type_compiler_cls = ExcelTypeCompiler
107
- preparer = ExcelIdentifierPreparer
111
+ preparer: type[IdentifierPreparer] = cast(
112
+ "type[IdentifierPreparer]", ExcelIdentifierPreparer
113
+ )
108
114
 
109
115
  @classmethod
110
116
  def import_dbapi(cls) -> Any:
@@ -140,7 +146,12 @@ class ExcelDialect(ExcelInspectionMixin, default.DefaultDialect): # type: ignor
140
146
  if "engine" in query:
141
147
  kwargs["engine"] = query.pop("engine")
142
148
  if "autocommit" in query:
143
- kwargs["autocommit"] = query.pop("autocommit").lower() in (
149
+ autocommit = query.pop("autocommit")
150
+ if isinstance(autocommit, tuple):
151
+ autocommit_text = autocommit[0] if autocommit else ""
152
+ else:
153
+ autocommit_text = autocommit
154
+ kwargs["autocommit"] = autocommit_text.lower() in (
144
155
  "true",
145
156
  "1",
146
157
  "yes",
@@ -180,7 +191,7 @@ class ExcelDialect(ExcelInspectionMixin, default.DefaultDialect): # type: ignor
180
191
  """Excel connections don't have network-level disconnects."""
181
192
  return False
182
193
 
183
- def get_default_isolation_level(self, dbapi_conn: Any) -> str:
194
+ def get_default_isolation_level(self, dbapi_conn: Any) -> Literal["SERIALIZABLE"]:
184
195
  return "SERIALIZABLE"
185
196
 
186
197
  def _check_unicode_returns(
@@ -202,7 +213,7 @@ class ExcelDialect(ExcelInspectionMixin, default.DefaultDialect): # type: ignor
202
213
  import excel_dbapi
203
214
 
204
215
  raw_conn = connection.connection.dbapi_connection
205
- return excel_dbapi.has_table(raw_conn, table_name)
216
+ return cast("bool", excel_dbapi.has_table(raw_conn, table_name))
206
217
 
207
218
  def do_begin(self, dbapi_connection: Any) -> None:
208
219
  """No-op: excel-dbapi doesn't have explicit BEGIN."""
@@ -6,11 +6,11 @@ tables (worksheets), columns, and primary keys from an Excel file.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from typing import Any
9
+ from typing import Any, cast
10
10
 
11
11
  from sqlalchemy import types as sa_types
12
12
 
13
- _TYPE_MAP: dict[str, type[sa_types.TypeEngine]] = {
13
+ _TYPE_MAP: dict[str, type[sa_types.TypeEngine[Any]]] = {
14
14
  "TEXT": sa_types.String,
15
15
  "INTEGER": sa_types.Integer,
16
16
  "FLOAT": sa_types.Float,
@@ -20,7 +20,7 @@ _TYPE_MAP: dict[str, type[sa_types.TypeEngine]] = {
20
20
  }
21
21
 
22
22
 
23
- def _sa_type_from_name(type_name: str) -> sa_types.TypeEngine:
23
+ def _sa_type_from_name(type_name: str) -> sa_types.TypeEngine[Any]:
24
24
  """Convert an excel-dbapi type name to a SQLAlchemy type instance."""
25
25
  cls = _TYPE_MAP.get(type_name.upper(), sa_types.String)
26
26
  return cls()
@@ -43,7 +43,7 @@ class ExcelInspectionMixin:
43
43
  import excel_dbapi
44
44
 
45
45
  raw_conn = connection.connection.dbapi_connection
46
- return excel_dbapi.list_tables(raw_conn, include_meta=False)
46
+ return cast("list[str]", excel_dbapi.list_tables(raw_conn, include_meta=False))
47
47
 
48
48
  def get_view_names(
49
49
  self,
@@ -134,6 +134,61 @@ class TestSelect:
134
134
  # Each row should have 2 columns
135
135
  assert len(rows[0]) == 2
136
136
 
137
+ def test_select_where_in(self, populated_engine, users_table):
138
+ with populated_engine.connect() as conn:
139
+ self._seed(conn, users_table)
140
+ stmt = select(users_table).where(
141
+ users_table.c.name.in_(["Alice", "Charlie"])
142
+ )
143
+ result = conn.execute(stmt)
144
+ rows = result.fetchall()
145
+ assert len(rows) == 2
146
+ names = {row[1] for row in rows}
147
+ assert names == {"Alice", "Charlie"}
148
+
149
+ def test_select_where_between(self, populated_engine, users_table):
150
+ with populated_engine.connect() as conn:
151
+ self._seed(conn, users_table)
152
+ stmt = select(users_table).where(users_table.c.age.between(26, 31))
153
+ result = conn.execute(stmt)
154
+ rows = result.fetchall()
155
+ assert len(rows) == 1
156
+ assert rows[0][1] == "Alice"
157
+
158
+ def test_select_where_like(self, populated_engine, users_table):
159
+ with populated_engine.connect() as conn:
160
+ self._seed(conn, users_table)
161
+ stmt = select(users_table).where(users_table.c.name.like("A%"))
162
+ result = conn.execute(stmt)
163
+ rows = result.fetchall()
164
+ assert len(rows) == 1
165
+ assert rows[0][1] == "Alice"
166
+
167
+ def test_select_where_like_contains(self, populated_engine, users_table):
168
+ with populated_engine.connect() as conn:
169
+ self._seed(conn, users_table)
170
+ stmt = select(users_table).where(users_table.c.name.like("%ob%"))
171
+ result = conn.execute(stmt)
172
+ rows = result.fetchall()
173
+ assert len(rows) == 1
174
+ assert rows[0][1] == "Bob"
175
+
176
+ def test_select_where_in_with_order_limit(self, populated_engine, users_table):
177
+ with populated_engine.connect() as conn:
178
+ self._seed(conn, users_table)
179
+ stmt = (
180
+ select(users_table)
181
+ .where(users_table.c.name.in_(["Alice", "Bob", "Charlie"]))
182
+ .order_by(users_table.c.age.desc())
183
+ .limit(2)
184
+ )
185
+ result = conn.execute(stmt)
186
+ rows = result.fetchall()
187
+ assert len(rows) == 2
188
+ # Charlie(35), Alice(30) — desc by age, limit 2
189
+ assert rows[0][1] == "Charlie"
190
+ assert rows[1][1] == "Alice"
191
+
137
192
 
138
193
  class TestUpdate:
139
194
  """Test UPDATE operations."""
@@ -1,96 +0,0 @@
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
@@ -1,64 +0,0 @@
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