Skip to content

Commit 7970219

Browse files
committed
Compiler: Add CrateIdentifierPreparer
By using this component of the SQLAlchemy dialect compiler, it can define CrateDB's reserved words to be quoted properly when building SQL statements. This allows to quote reserved words like `index` or `object` properly, for example when used as column names.
1 parent 877ebaa commit 7970219

File tree

4 files changed

+75
-1
lines changed

4 files changed

+75
-1
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
## Unreleased
55
- Added/reactivated documentation as `sqlalchemy-cratedb`
6+
- Added `CrateIdentifierPreparer`, in order to quote reserved words
7+
like `object` properly, for example when used as column names.
68

79
## 2024/06/13 0.37.0
810
- Added support for CrateDB's [FLOAT_VECTOR] data type and its accompanying

src/sqlalchemy_cratedb/compiler.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import sqlalchemy as sa
2727
from sqlalchemy.dialects.postgresql.base import PGCompiler
28+
from sqlalchemy.dialects.postgresql.base import RESERVED_WORDS as POSTGRESQL_RESERVED_WORDS
2829
from sqlalchemy.sql import compiler
2930
from sqlalchemy.types import String
3031
from .type.geo import Geopoint, Geoshape
@@ -323,3 +324,44 @@ def for_update_clause(self, select, **kw):
323324
warnings.warn("CrateDB does not support the 'INSERT ... FOR UPDATE' clause, "
324325
"it will be omitted when generating SQL statements.")
325326
return ''
327+
328+
329+
CRATEDB_RESERVED_WORDS = \
330+
"cross, current_date, intersect, else, end, except, using, case, and, current_schema, any, " \
331+
"all, set, limit, input, natural, cast, directory, is, when, if, table, right, outer, full, " \
332+
"order, select, join, add, session_user, current_time, grant, true, left, into, try_cast, " \
333+
"current_role, insert, some, exists, update, false, create, reset, offset, object, " \
334+
"transient, current_user, in, or, for, alter, asc, function, null, from, default, not, " \
335+
"like, union, distinct, nulls, having, inner, by, persistent, stratify, array, revoke, " \
336+
"match, drop, escape, where, costs, with, group, index, delete, column, on, unbounded, " \
337+
"returns, then, last, user, called, recursive, between, describe, as, extract, " \
338+
"current_timestamp, deny, first, constraint, desc".split(", ")
339+
340+
341+
class CrateIdentifierPreparer(sa.sql.compiler.IdentifierPreparer):
342+
"""
343+
Define CrateDB's reserved words to be quoted properly.
344+
"""
345+
reserved_words = set(list(POSTGRESQL_RESERVED_WORDS) + CRATEDB_RESERVED_WORDS)
346+
347+
def _unquote_identifier(self, value):
348+
if value[0] == self.initial_quote:
349+
value = value[1:-1].replace(
350+
self.escape_to_quote, self.escape_quote
351+
)
352+
return value
353+
354+
def format_type(self, type_, use_schema=True):
355+
if not type_.name:
356+
raise sa.exc.CompileError("Type requires a name.")
357+
358+
name = self.quote(type_.name)
359+
effective_schema = self.schema_for_object(type_)
360+
361+
if (
362+
not self.omit_schema
363+
and use_schema
364+
and effective_schema is not None
365+
):
366+
name = self.quote_schema(effective_schema) + "." + name
367+
return name

src/sqlalchemy_cratedb/dialect.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929

3030
from .compiler import (
3131
CrateTypeCompiler,
32-
CrateDDLCompiler
32+
CrateDDLCompiler,
33+
CrateIdentifierPreparer,
3334
)
3435
from crate.client.exceptions import TimezoneUnawareException
3536
from .sa_version import SA_VERSION, SA_1_4, SA_2_0
@@ -174,6 +175,7 @@ class CrateDialect(default.DefaultDialect):
174175
statement_compiler = statement_compiler
175176
ddl_compiler = CrateDDLCompiler
176177
type_compiler = CrateTypeCompiler
178+
preparer = CrateIdentifierPreparer
177179
use_insertmanyvalues = True
178180
use_insertmanyvalues_wo_returning = True
179181
supports_multivalues_insert = True

tests/compiler_test.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,3 +432,31 @@ class FooBar(Base):
432432
self.assertIsSubclass(w[-1].category, UserWarning)
433433
self.assertIn("CrateDB does not support unique constraints, "
434434
"they will be omitted when generating DDL statements.", str(w[-1].message))
435+
436+
def test_ddl_with_reserved_words(self):
437+
"""
438+
Verify CrateDB's reserved words like `object` are quoted properly.
439+
"""
440+
441+
Base = declarative_base(metadata=self.metadata)
442+
443+
class FooBar(Base):
444+
"""The entity."""
445+
446+
__tablename__ = "foobar"
447+
448+
index = sa.Column(sa.Integer, primary_key=True)
449+
array = sa.Column(sa.String)
450+
object = sa.Column(sa.String)
451+
452+
# Verify SQL DDL statement.
453+
self.metadata.create_all(self.engine, tables=[FooBar.__table__], checkfirst=False)
454+
self.assertEqual(self.executed_statement, dedent("""
455+
CREATE TABLE testdrive.foobar (
456+
\t"index" INT NOT NULL,
457+
\t"array" STRING,
458+
\t"object" STRING,
459+
\tPRIMARY KEY ("index")
460+
)
461+
462+
""")) # noqa: W291, W293

0 commit comments

Comments
 (0)