sqlalchemy-excel 0.1.1__py3-none-any.whl

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.
@@ -0,0 +1,12 @@
1
+ """sqlalchemy-excel — SQLAlchemy dialect for Excel files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .dialect import ExcelDialect
6
+
7
+ __version__ = "0.1.1"
8
+
9
+ __all__ = [
10
+ "ExcelDialect",
11
+ "__version__",
12
+ ]
@@ -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")
@@ -0,0 +1,221 @@
1
+ """ExcelDialect — SQLAlchemy dialect that uses excel-dbapi as the DB-API driver."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import TYPE_CHECKING, Any, ClassVar
7
+
8
+ from sqlalchemy import event, pool
9
+ from sqlalchemy.engine import default
10
+ from sqlalchemy.schema import Table
11
+
12
+ from .compiler import ExcelCompiler, ExcelIdentifierPreparer
13
+ from .ddl import ExcelDDLCompiler
14
+ from .reflection import ExcelInspectionMixin
15
+ from .types import ExcelTypeCompiler
16
+
17
+ if TYPE_CHECKING:
18
+ from sqlalchemy.engine import URL
19
+ from sqlalchemy.engine.interfaces import ConnectArgsType
20
+
21
+
22
+ def _after_create(
23
+ target: Table,
24
+ connection: Any,
25
+ **kw: Any,
26
+ ) -> None:
27
+ """Write column metadata after CREATE TABLE (Excel dialect only)."""
28
+ if connection.dialect.name != "excel":
29
+ return
30
+ import excel_dbapi
31
+
32
+ raw_conn = connection.connection.dbapi_connection
33
+ pk_cols = {col.name for col in target.primary_key.columns}
34
+
35
+ columns = []
36
+ for col in target.columns:
37
+ type_compiler = connection.dialect.type_compiler
38
+ type_name = type_compiler.process(col.type)
39
+ columns.append(
40
+ {
41
+ "name": col.name,
42
+ "type_name": type_name,
43
+ "nullable": col.nullable if col.nullable is not None else True,
44
+ "primary_key": col.name in pk_cols,
45
+ }
46
+ )
47
+
48
+ excel_dbapi.write_table_metadata(raw_conn, target.name, columns)
49
+
50
+
51
+ def _after_drop(
52
+ target: Table,
53
+ connection: Any,
54
+ **kw: Any,
55
+ ) -> None:
56
+ """Remove column metadata after DROP TABLE (Excel dialect only)."""
57
+ if connection.dialect.name != "excel":
58
+ return
59
+ import excel_dbapi
60
+
61
+ raw_conn = connection.connection.dbapi_connection
62
+ excel_dbapi.remove_table_metadata(raw_conn, target.name)
63
+
64
+
65
+ # Register DDL events globally for all Table objects
66
+ event.listen(Table, "after_create", _after_create)
67
+ event.listen(Table, "after_drop", _after_drop)
68
+
69
+
70
+ class ExcelDialect(ExcelInspectionMixin, default.DefaultDialect): # type: ignore[misc]
71
+ """SQLAlchemy dialect for Excel files via excel-dbapi.
72
+
73
+ Connection URLs::
74
+
75
+ # Relative path
76
+ excel:///data.xlsx
77
+
78
+ # Absolute path
79
+ excel:////home/user/data.xlsx
80
+ excel:///C:/Users/data.xlsx (Windows)
81
+
82
+ """
83
+
84
+ name: ClassVar[str] = "excel"
85
+ driver: ClassVar[str] = "dbapi"
86
+ default_paramstyle: ClassVar[str] = "qmark"
87
+
88
+ # ── 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
102
+
103
+ # ── Compiler classes ──────────────────────────────────
104
+ statement_compiler = ExcelCompiler
105
+ ddl_compiler = ExcelDDLCompiler
106
+ type_compiler_cls = ExcelTypeCompiler
107
+ preparer = ExcelIdentifierPreparer
108
+
109
+ @classmethod
110
+ def import_dbapi(cls) -> Any:
111
+ import excel_dbapi
112
+
113
+ return excel_dbapi
114
+
115
+ @classmethod
116
+ def get_pool_class(cls, url: URL) -> type[pool.Pool]:
117
+ return pool.StaticPool
118
+
119
+ def create_connect_args(self, url: URL) -> ConnectArgsType:
120
+ """Translate a SQLAlchemy URL to excel-dbapi connect() arguments.
121
+
122
+ URL formats:
123
+ excel:///relative/path.xlsx → file_path="relative/path.xlsx"
124
+ excel:////absolute/path.xlsx → file_path="/absolute/path.xlsx"
125
+ """
126
+ # url.database contains the path after the third slash
127
+ file_path = url.database
128
+ if not file_path:
129
+ raise ValueError("No file path in URL. Use excel:///path/to/file.xlsx")
130
+
131
+ kwargs: dict[str, Any] = {
132
+ "file_path": file_path,
133
+ "engine": "openpyxl",
134
+ "autocommit": False,
135
+ "create": True,
136
+ }
137
+
138
+ # Forward query parameters
139
+ query = dict(url.query)
140
+ if "engine" in query:
141
+ kwargs["engine"] = query.pop("engine")
142
+ if "autocommit" in query:
143
+ kwargs["autocommit"] = query.pop("autocommit").lower() in (
144
+ "true",
145
+ "1",
146
+ "yes",
147
+ )
148
+
149
+ return ([], kwargs)
150
+
151
+ def on_connect(self) -> None:
152
+ """No-op: no special connection initialization needed."""
153
+
154
+ def do_execute(
155
+ self,
156
+ cursor: Any,
157
+ statement: str,
158
+ parameters: Any,
159
+ context: Any = None,
160
+ ) -> None:
161
+ """Execute a statement, normalizing whitespace for excel-dbapi."""
162
+ normalized = re.sub(r"\s+", " ", statement).strip()
163
+ cursor.execute(normalized, parameters)
164
+
165
+ def do_execute_no_params(
166
+ self,
167
+ cursor: Any,
168
+ statement: str,
169
+ context: Any = None,
170
+ ) -> None:
171
+ """Execute a statement with no parameters."""
172
+ normalized = re.sub(r"\s+", " ", statement).strip()
173
+ cursor.execute(normalized, None)
174
+
175
+ def do_ping(self, dbapi_connection: Any) -> bool:
176
+ """Ping the connection by verifying it's not closed."""
177
+ return not getattr(dbapi_connection, "closed", True)
178
+
179
+ def is_disconnect(self, e: Exception, connection: Any, cursor: Any) -> bool:
180
+ """Excel connections don't have network-level disconnects."""
181
+ return False
182
+
183
+ def get_default_isolation_level(self, dbapi_conn: Any) -> str:
184
+ return "SERIALIZABLE"
185
+
186
+ def _check_unicode_returns(
187
+ self, connection: Any, additional_tests: Any = None
188
+ ) -> bool:
189
+ return True
190
+
191
+ def _check_unicode_description(self, connection: Any) -> bool:
192
+ return True
193
+
194
+ def has_table(
195
+ self,
196
+ connection: Any,
197
+ table_name: str,
198
+ schema: str | None = None,
199
+ **kw: Any,
200
+ ) -> bool:
201
+ """Check if a worksheet (table) exists."""
202
+ import excel_dbapi
203
+
204
+ raw_conn = connection.connection.dbapi_connection
205
+ return excel_dbapi.has_table(raw_conn, table_name)
206
+
207
+ def do_begin(self, dbapi_connection: Any) -> None:
208
+ """No-op: excel-dbapi doesn't have explicit BEGIN."""
209
+
210
+ def do_commit(self, dbapi_connection: Any) -> None:
211
+ """Commit (save) the workbook."""
212
+ dbapi_connection.commit()
213
+
214
+ def do_rollback(self, dbapi_connection: Any) -> None:
215
+ """Rollback is not supported with autocommit=False in a meaningful way
216
+ for Excel files. We silently ignore it to avoid crashes during
217
+ connection pool cleanup."""
218
+
219
+ def do_close(self, dbapi_connection: Any) -> None:
220
+ """Close the underlying excel-dbapi connection."""
221
+ dbapi_connection.close()
@@ -0,0 +1,173 @@
1
+ """Reflection (introspection) support for the Excel dialect.
2
+
3
+ Provides the Inspector integration so that SQLAlchemy can discover
4
+ tables (worksheets), columns, and primary keys from an Excel file.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from sqlalchemy import types as sa_types
12
+
13
+ _TYPE_MAP: dict[str, type[sa_types.TypeEngine]] = {
14
+ "TEXT": sa_types.String,
15
+ "INTEGER": sa_types.Integer,
16
+ "FLOAT": sa_types.Float,
17
+ "BOOLEAN": sa_types.Boolean,
18
+ "DATE": sa_types.Date,
19
+ "DATETIME": sa_types.DateTime,
20
+ }
21
+
22
+
23
+ def _sa_type_from_name(type_name: str) -> sa_types.TypeEngine:
24
+ """Convert an excel-dbapi type name to a SQLAlchemy type instance."""
25
+ cls = _TYPE_MAP.get(type_name.upper(), sa_types.String)
26
+ return cls()
27
+
28
+
29
+ class ExcelInspectionMixin:
30
+ """Mixin that provides reflection methods for ExcelDialect.
31
+
32
+ These methods are called by SQLAlchemy's Inspector to discover
33
+ the structure of an Excel workbook.
34
+ """
35
+
36
+ def get_table_names(
37
+ self,
38
+ connection: Any,
39
+ schema: str | None = None,
40
+ **kw: Any,
41
+ ) -> list[str]:
42
+ """Return all worksheet names, excluding the metadata sheet."""
43
+ import excel_dbapi
44
+
45
+ raw_conn = connection.connection.dbapi_connection
46
+ return excel_dbapi.list_tables(raw_conn, include_meta=False)
47
+
48
+ def get_view_names(
49
+ self,
50
+ connection: Any,
51
+ schema: str | None = None,
52
+ **kw: Any,
53
+ ) -> list[str]:
54
+ """Excel does not support views."""
55
+ return []
56
+
57
+ def get_columns(
58
+ self,
59
+ connection: Any,
60
+ table_name: str,
61
+ schema: str | None = None,
62
+ **kw: Any,
63
+ ) -> list[dict[str, Any]]:
64
+ """Return column information for the given table (worksheet).
65
+
66
+ First tries to read from the metadata sheet (written by CREATE TABLE).
67
+ Falls back to type inference from data sampling.
68
+ """
69
+ import excel_dbapi
70
+
71
+ raw_conn = connection.connection.dbapi_connection
72
+
73
+ # Try metadata sheet first
74
+ meta = excel_dbapi.read_table_metadata(raw_conn, table_name)
75
+ if meta is not None:
76
+ return [
77
+ {
78
+ "name": col["name"],
79
+ "type": _sa_type_from_name(col["type_name"]),
80
+ "nullable": col.get("nullable", True),
81
+ "default": None,
82
+ "autoincrement": False,
83
+ "comment": None,
84
+ }
85
+ for col in meta
86
+ ]
87
+
88
+ # Fallback: infer from data
89
+ inferred = excel_dbapi.get_columns(raw_conn, table_name)
90
+ return [
91
+ {
92
+ "name": col["name"],
93
+ "type": _sa_type_from_name(col["type"]),
94
+ "nullable": col.get("nullable", True),
95
+ "default": None,
96
+ "autoincrement": False,
97
+ "comment": None,
98
+ }
99
+ for col in inferred
100
+ ]
101
+
102
+ def get_pk_constraint(
103
+ self,
104
+ connection: Any,
105
+ table_name: str,
106
+ schema: str | None = None,
107
+ **kw: Any,
108
+ ) -> dict[str, Any]:
109
+ """Return primary key constraint info from the metadata sheet."""
110
+ import excel_dbapi
111
+
112
+ raw_conn = connection.connection.dbapi_connection
113
+ meta = excel_dbapi.read_table_metadata(raw_conn, table_name)
114
+ if meta is not None:
115
+ pk_cols = [col["name"] for col in meta if col.get("primary_key", False)]
116
+ if pk_cols:
117
+ return {"constrained_columns": pk_cols, "name": None}
118
+
119
+ return {"constrained_columns": [], "name": None}
120
+
121
+ def get_foreign_keys(
122
+ self,
123
+ connection: Any,
124
+ table_name: str,
125
+ schema: str | None = None,
126
+ **kw: Any,
127
+ ) -> list[dict[str, Any]]:
128
+ """Excel does not support foreign keys."""
129
+ return []
130
+
131
+ def get_indexes(
132
+ self,
133
+ connection: Any,
134
+ table_name: str,
135
+ schema: str | None = None,
136
+ **kw: Any,
137
+ ) -> list[dict[str, Any]]:
138
+ """Excel does not support indexes."""
139
+ return []
140
+
141
+ def get_unique_constraints(
142
+ self,
143
+ connection: Any,
144
+ table_name: str,
145
+ schema: str | None = None,
146
+ **kw: Any,
147
+ ) -> list[dict[str, Any]]:
148
+ """Excel does not support unique constraints."""
149
+ return []
150
+
151
+ def get_check_constraints(
152
+ self,
153
+ connection: Any,
154
+ table_name: str,
155
+ schema: str | None = None,
156
+ **kw: Any,
157
+ ) -> list[dict[str, Any]]:
158
+ """Excel does not support check constraints."""
159
+ return []
160
+
161
+ def get_table_comment(
162
+ self,
163
+ connection: Any,
164
+ table_name: str,
165
+ schema: str | None = None,
166
+ **kw: Any,
167
+ ) -> dict[str, Any]:
168
+ """Excel does not support table comments."""
169
+ return {"text": None}
170
+
171
+ def get_schema_names(self, connection: Any, **kw: Any) -> list[str]:
172
+ """Excel does not support schemas."""
173
+ return []
@@ -0,0 +1,100 @@
1
+ """Type compiler for the Excel dialect.
2
+
3
+ Maps SQLAlchemy types to the type names that excel-dbapi understands:
4
+ TEXT, INTEGER, FLOAT, BOOLEAN, DATE, DATETIME
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from sqlalchemy import exc
12
+ from sqlalchemy.sql import compiler
13
+
14
+
15
+ class ExcelTypeCompiler(compiler.GenericTypeCompiler):
16
+ """Maps SQLAlchemy column types to Excel-compatible type strings."""
17
+
18
+ def visit_STRING(self, type_: Any, **kw: Any) -> str:
19
+ return "TEXT"
20
+
21
+ def visit_TEXT(self, type_: Any, **kw: Any) -> str:
22
+ return "TEXT"
23
+
24
+ def visit_NVARCHAR(self, type_: Any, **kw: Any) -> str:
25
+ return "TEXT"
26
+
27
+ def visit_VARCHAR(self, type_: Any, **kw: Any) -> str:
28
+ return "TEXT"
29
+
30
+ def visit_CHAR(self, type_: Any, **kw: Any) -> str:
31
+ return "TEXT"
32
+
33
+ def visit_NCHAR(self, type_: Any, **kw: Any) -> str:
34
+ return "TEXT"
35
+
36
+ def visit_CLOB(self, type_: Any, **kw: Any) -> str:
37
+ return "TEXT"
38
+
39
+ def visit_INTEGER(self, type_: Any, **kw: Any) -> str:
40
+ return "INTEGER"
41
+
42
+ def visit_SMALLINT(self, type_: Any, **kw: Any) -> str:
43
+ return "INTEGER"
44
+
45
+ def visit_BIGINT(self, type_: Any, **kw: Any) -> str:
46
+ return "INTEGER"
47
+
48
+ def visit_FLOAT(self, type_: Any, **kw: Any) -> str:
49
+ return "FLOAT"
50
+
51
+ def visit_REAL(self, type_: Any, **kw: Any) -> str:
52
+ return "FLOAT"
53
+
54
+ def visit_DOUBLE(self, type_: Any, **kw: Any) -> str:
55
+ return "FLOAT"
56
+
57
+ def visit_DOUBLE_PRECISION(self, type_: Any, **kw: Any) -> str:
58
+ return "FLOAT"
59
+
60
+ def visit_NUMERIC(self, type_: Any, **kw: Any) -> str:
61
+ return "FLOAT"
62
+
63
+ def visit_DECIMAL(self, type_: Any, **kw: Any) -> str:
64
+ return "FLOAT"
65
+
66
+ def visit_BOOLEAN(self, type_: Any, **kw: Any) -> str:
67
+ return "BOOLEAN"
68
+
69
+ def visit_DATE(self, type_: Any, **kw: Any) -> str:
70
+ return "DATE"
71
+
72
+ def visit_DATETIME(self, type_: Any, **kw: Any) -> str:
73
+ return "DATETIME"
74
+
75
+ def visit_TIMESTAMP(self, type_: Any, **kw: Any) -> str:
76
+ return "DATETIME"
77
+
78
+ def visit_TIME(self, type_: Any, **kw: Any) -> str:
79
+ return "TEXT"
80
+
81
+ def visit_BLOB(self, type_: Any, **kw: Any) -> str:
82
+ raise exc.CompileError("Excel dialect does not support BLOB type")
83
+
84
+ def visit_BINARY(self, type_: Any, **kw: Any) -> str:
85
+ raise exc.CompileError("Excel dialect does not support BINARY type")
86
+
87
+ def visit_VARBINARY(self, type_: Any, **kw: Any) -> str:
88
+ raise exc.CompileError("Excel dialect does not support VARBINARY type")
89
+
90
+ def visit_JSON(self, type_: Any, **kw: Any) -> str:
91
+ raise exc.CompileError("Excel dialect does not support JSON type")
92
+
93
+ def visit_ARRAY(self, type_: Any, **kw: Any) -> str:
94
+ raise exc.CompileError("Excel dialect does not support ARRAY type")
95
+
96
+ def visit_large_binary(self, type_: Any, **kw: Any) -> str:
97
+ raise exc.CompileError("Excel dialect does not support LargeBinary type")
98
+
99
+ def visit_uuid(self, type_: Any, **kw: Any) -> str:
100
+ return "TEXT"
@@ -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,11 @@
1
+ sqlalchemy_excel/__init__.py,sha256=EmKZ04CUp9__0xiVTtpvNfAsAnYwuDsmgrSqDn9xBVg,211
2
+ sqlalchemy_excel/compiler.py,sha256=UIb3C-m6iEDMxiuNvjzoKGJc_x7dBnFar4pD04FpciY,5511
3
+ sqlalchemy_excel/ddl.py,sha256=7t-huNzOvCtsYL71DWvC3bnT_ufOKgq_U6HsL-wTFWA,2117
4
+ sqlalchemy_excel/dialect.py,sha256=F5u0Eqi4XFXAX-d-OfaznSIIgwjKM3zUdoFe_crWBRs,7174
5
+ sqlalchemy_excel/reflection.py,sha256=JO9qS7uzwXabtScRB9XObLxs6X8K1rCCNmGWBl2GdVI,5026
6
+ sqlalchemy_excel/types.py,sha256=FnnhRuxKhBamQxmvyZFuiYzE-gQd_1LGkIDfpnL0Yko,3058
7
+ sqlalchemy_excel-0.1.1.dist-info/METADATA,sha256=UrMik9qyZJzQygxz3IPAr2xWc9wBHfR_JCCAMJrcxUw,2734
8
+ sqlalchemy_excel-0.1.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
+ sqlalchemy_excel-0.1.1.dist-info/entry_points.txt,sha256=Bl0Hy4J_V012f3oe-WPevI-KE5rVyaqMi_zYBiCIjkE,68
10
+ sqlalchemy_excel-0.1.1.dist-info/licenses/LICENSE,sha256=gBTzk1NFKuyZKqa5-8leVY816k7POZUMpw2_PKl2MsY,1071
11
+ sqlalchemy_excel-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [sqlalchemy.dialects]
2
+ excel = sqlalchemy_excel.dialect:ExcelDialect
@@ -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.