Skip to content

Commit 050c93e

Browse files
committed
fix: close oracle round 8 DDL and reflection gaps
1 parent 707b694 commit 050c93e

11 files changed

Lines changed: 317 additions & 71 deletions

File tree

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Before writing any code, understand the dialect's capabilities and limits:
4444
| **ALTER TABLE** | ✅ (ADD/DROP/RENAME COLUMN) |
4545
| Raw DDL metadata sync (`text()` / `exec_driver_sql()`) | ✅ (`CREATE TABLE` / `ALTER TABLE` / `DROP TABLE`) |
4646
| **Foreign keys / indexes** ||
47-
| **UNIQUE / CHECK enforcement** | ❌ (accepted for compatibility; warnings are emitted) |
47+
| **UNIQUE / CHECK enforcement** | ❌ (accepted for compatibility; compile-time warnings are emitted for CREATE TABLE and ALTER TABLE ADD COLUMN) |
4848
| **Concurrent writes** ||
4949
| **Session.rollback()** | Partial (openpyxl with autocommit=False; graph: no-op) |
5050

@@ -252,8 +252,11 @@ If metadata remains for a deleted worksheet, sqlalchemy-excel removes the stale
252252
entry and returns an empty inspection result for that table name.
253253

254254
Raw DDL executed through `text()` or `exec_driver_sql()` keeps metadata in sync
255-
for `CREATE TABLE`, `ALTER TABLE`, and `DROP TABLE`, so declared types
256-
round-trip through reflection.
255+
for `CREATE TABLE`, `ALTER TABLE`, and `DROP TABLE`.
256+
257+
- Primary keys declared inline or as table-level composite constraints are reflected.
258+
- Numeric declarations `FLOAT`, `REAL`, `DECIMAL`, `NUMERIC`, `DOUBLE`, and
259+
`DOUBLE PRECISION` are reflected as SQLAlchemy `Float`.
257260

258261
```python
259262
from sqlalchemy import create_engine, inspect

docs/USAGE.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,9 @@ SQLAlchemy's inspector API works with Excel files:
302302
- Stale metadata for deleted worksheets is automatically cleaned up.
303303
- Raw DDL via `text()` / `exec_driver_sql()` keeps metadata synchronized for
304304
`CREATE TABLE`, `ALTER TABLE`, and `DROP TABLE`.
305+
- Table-level composite `PRIMARY KEY (...)` declarations are reflected.
306+
- Raw numeric declarations `FLOAT`, `REAL`, `DECIMAL`, `NUMERIC`, `DOUBLE`,
307+
and `DOUBLE PRECISION` are reflected as SQLAlchemy `Float`.
305308

306309
```python
307310
from sqlalchemy import create_engine, inspect
@@ -331,7 +334,7 @@ sqlalchemy-excel maps SQLAlchemy types to Excel storage types:
331334
|-----------------|---------------|-------|
332335
| `String`, `Text`, `VARCHAR`, `CHAR` | TEXT | All string types → TEXT |
333336
| `Integer`, `SmallInteger`, `BigInteger` | INTEGER | All integer types → INTEGER |
334-
| `Float`, `Numeric`, `Decimal` | FLOAT | All numeric types → FLOAT |
337+
| `Float`, `Numeric`, `Decimal`, `DOUBLE`, `DOUBLE PRECISION` | FLOAT | Reflected as SQLAlchemy `Float` |
335338
| `Boolean` | BOOLEAN | Stored as boolean |
336339
| `Date` | DATE | Date without time |
337340
| `DateTime`, `TIMESTAMP` | DATETIME | Date with time |
@@ -353,7 +356,7 @@ sqlalchemy-excel has some limitations due to the nature of Excel as a database:
353356
- **ORM relationship limits**: Lazy one-to-many relationship loading can return empty collections; use eager loading (`joinedload`) for reliable one-to-many reads.
354357
- **Many-to-many loading is unsupported**: Association table persistence works, but relationship loader SQL for many-to-many is not fully supported.
355358
- **No foreign keys or indexes**: Excel has no concept of these.
356-
- **UNIQUE/CHECK are not enforced**: They are accepted for SQLAlchemy compatibility, ignored by the backend, and compile-time warnings are emitted.
359+
- **UNIQUE/CHECK are not enforced**: They are accepted for SQLAlchemy compatibility, ignored by the backend, and compile-time warnings are emitted for `CREATE TABLE` and `ALTER TABLE ... ADD COLUMN`.
357360
- **Identifier restrictions**: Table and column names must match `[A-Za-z_][A-Za-z0-9_]*`.
358361
- **No concurrent writes**: Use a single-writer model.
359362
- **Rollback**: Partial support — works with the openpyxl backend when `autocommit=False` (snapshot/restore semantics). The Graph API backend treats rollback as a no-op.

src/sqlalchemy_excel/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44

55
from importlib.metadata import PackageNotFoundError, version
66

7+
from sqlalchemy.dialects import registry
8+
79
from .dialect import ExcelDialect, ExcelGraphDialect
810
from .dml import Insert, insert
911

12+
registry.register("excel", "sqlalchemy_excel.dialect", "ExcelDialect")
13+
registry.register("excel.graph", "sqlalchemy_excel.dialect", "ExcelGraphDialect")
14+
1015
try:
1116
__version__ = version("sqlalchemy-excel")
1217
except PackageNotFoundError:

src/sqlalchemy_excel/ddl.py

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,42 @@
1818
class ExcelDDLCompiler(compiler.DDLCompiler):
1919
"""Compiles DDL statements for excel-dbapi."""
2020

21+
@staticmethod
22+
def _warn_unsupported_constraints(
23+
column: Any,
24+
context_msg: str,
25+
seen_unique: set[tuple[str, ...]],
26+
seen_check: set[str],
27+
) -> None:
28+
if column.unique is True:
29+
key = (column.name,)
30+
if key not in seen_unique:
31+
warnings.warn(
32+
f"Excel dialect does not enforce UNIQUE constraints ({context_msg})",
33+
stacklevel=2,
34+
)
35+
seen_unique.add(key)
36+
37+
for constraint in column.constraints:
38+
if isinstance(constraint, UniqueConstraint):
39+
unique_key = tuple(col.name for col in constraint.columns) or (
40+
column.name,
41+
)
42+
if unique_key not in seen_unique:
43+
warnings.warn(
44+
f"Excel dialect does not enforce UNIQUE constraints ({context_msg})",
45+
stacklevel=2,
46+
)
47+
seen_unique.add(unique_key)
48+
if isinstance(constraint, CheckConstraint):
49+
check_key = str(constraint.sqltext)
50+
if check_key not in seen_check:
51+
warnings.warn(
52+
f"Excel dialect does not enforce CHECK constraints ({context_msg})",
53+
stacklevel=2,
54+
)
55+
seen_check.add(check_key)
56+
2157
def _format_alter_table_name(self, operation: Any) -> str:
2258
schema = getattr(operation, "schema", None)
2359
if schema is not None:
@@ -48,34 +84,49 @@ def visit_create_table(self, create: Any, **kw: Any) -> str:
4884

4985
columns = []
5086
pk_columns = {col.name for col in table.primary_key.columns}
87+
pk_columns_ordered = [
88+
self.preparer.format_column(col) for col in table.primary_key.columns
89+
]
5190
inline_pk = len(pk_columns) == 1
91+
seen_unique: set[tuple[str, ...]] = set()
92+
seen_check: set[str] = set()
5293
for col in table.columns:
5394
col_name = self.preparer.format_column(col)
5495
col_type = self.dialect.type_compiler.process(col.type)
5596
constraints: list[str] = []
56-
if col.nullable is False:
97+
if col.nullable is False and col.name not in pk_columns:
5798
constraints.append("NOT NULL")
5899
if inline_pk and col.name in pk_columns:
59100
constraints.append("PRIMARY KEY")
60-
if col.unique is True:
61-
warnings.warn(
62-
"Excel dialect does not enforce UNIQUE constraints",
63-
stacklevel=3,
64-
)
101+
self._warn_unsupported_constraints(
102+
col,
103+
context_msg="CREATE TABLE",
104+
seen_unique=seen_unique,
105+
seen_check=seen_check,
106+
)
65107
suffix = f" {' '.join(constraints)}" if constraints else ""
66108
columns.append(f"{col_name} {col_type}{suffix}")
67109

110+
if len(pk_columns_ordered) > 1:
111+
columns.append(f"PRIMARY KEY ({', '.join(pk_columns_ordered)})")
112+
68113
for constraint in table.constraints:
69114
if isinstance(constraint, UniqueConstraint):
70-
warnings.warn(
71-
"Excel dialect does not enforce UNIQUE constraints",
72-
stacklevel=3,
73-
)
115+
unique_key = tuple(col.name for col in constraint.columns)
116+
if unique_key not in seen_unique:
117+
warnings.warn(
118+
"Excel dialect does not enforce UNIQUE constraints (CREATE TABLE)",
119+
stacklevel=2,
120+
)
121+
seen_unique.add(unique_key)
74122
if isinstance(constraint, CheckConstraint):
75-
warnings.warn(
76-
"Excel dialect does not enforce CHECK constraints",
77-
stacklevel=3,
78-
)
123+
check_key = str(constraint.sqltext)
124+
if check_key not in seen_check:
125+
warnings.warn(
126+
"Excel dialect does not enforce CHECK constraints (CREATE TABLE)",
127+
stacklevel=2,
128+
)
129+
seen_check.add(check_key)
79130

80131
return f"CREATE TABLE {table_name} ({', '.join(columns)})"
81132

@@ -94,10 +145,16 @@ def visit_add_column(self, create: Any, **kw: Any) -> str:
94145
col_name = self.preparer.format_column(column)
95146
col_type = self.dialect.type_compiler.process(column.type)
96147
constraints: list[str] = []
97-
if column.nullable is False:
148+
if column.nullable is False and column.primary_key is not True:
98149
constraints.append("NOT NULL")
99150
if column.primary_key is True:
100151
constraints.append("PRIMARY KEY")
152+
self._warn_unsupported_constraints(
153+
column,
154+
context_msg="ALTER TABLE ADD COLUMN",
155+
seen_unique=set(),
156+
seen_check=set(),
157+
)
101158
suffix = f" {' '.join(constraints)}" if constraints else ""
102159
return f"ALTER TABLE {table_name} ADD COLUMN {col_name} {col_type}{suffix}"
103160

0 commit comments

Comments
 (0)