Skip to content

Commit e17ba04

Browse files
committed
Add quote_relation_name support utility function
1 parent 473455d commit e17ba04

File tree

4 files changed

+93
-2
lines changed

4 files changed

+93
-2
lines changed

CHANGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Changelog
22

3-
43
## Unreleased
4+
- Added `quote_relation_name` support utility function
55

66
## 2024/06/25 0.38.0
77
- Added/reactivated documentation as `sqlalchemy-cratedb`

src/sqlalchemy_cratedb/support/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
patch_autoincrement_timestamp,
55
refresh_after_dml,
66
)
7-
from sqlalchemy_cratedb.support.util import refresh_dirty, refresh_table
7+
from sqlalchemy_cratedb.support.util import quote_relation_name, refresh_dirty, refresh_table
88

99
__all__ = [
1010
check_uniqueness_factory,
1111
insert_bulk,
1212
patch_autoincrement_timestamp,
13+
quote_relation_name,
1314
refresh_after_dml,
1415
refresh_dirty,
1516
refresh_table,

src/sqlalchemy_cratedb/support/util.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@
33

44
import sqlalchemy as sa
55

6+
from sqlalchemy_cratedb.dialect import CrateDialect
7+
68
if t.TYPE_CHECKING:
79
try:
810
from sqlalchemy.orm import DeclarativeBase
911
except ImportError:
1012
pass
1113

1214

15+
# An instance of the dialect used for quoting purposes.
16+
identifier_preparer = CrateDialect().identifier_preparer
17+
18+
1319
def refresh_table(
1420
connection, target: t.Union[str, "DeclarativeBase", "sa.sql.selectable.TableClause"]
1521
):
@@ -41,3 +47,36 @@ def refresh_dirty(session, flush_context=None):
4147
dirty_classes = {entity.__class__ for entity in dirty_entities}
4248
for class_ in dirty_classes:
4349
refresh_table(session, class_)
50+
51+
52+
def quote_relation_name(ident: str) -> str:
53+
"""
54+
Quote a simple or full-qualified table/relation name, when needed.
55+
56+
Simple: <table>
57+
Full-qualified: <schema>.<table>
58+
59+
Happy path examples:
60+
61+
foo => foo
62+
Foo => "Foo"
63+
"Foo" => "Foo"
64+
foo.bar => foo.bar
65+
foo-bar.baz_qux => "foo-bar".baz_qux
66+
67+
Such input strings will not be modified:
68+
69+
"foo.bar" => "foo.bar"
70+
"""
71+
72+
# If a quote exists at the beginning or the end of the input string,
73+
# let's consider that the relation name has been quoted already.
74+
if ident.startswith('"') or ident.endswith('"'):
75+
return ident
76+
77+
# If a dot is included, it's a full-qualified identifier like <schema>.<table>.
78+
# It needs to be split, in order to apply identifier quoting properly.
79+
parts = ident.split(".")
80+
if len(parts) > 3:
81+
raise ValueError(f"Invalid relation name, too many parts: {ident}")
82+
return ".".join(map(identifier_preparer.quote, parts))

tests/test_support_util.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import pytest
2+
3+
from sqlalchemy_cratedb.support import quote_relation_name
4+
5+
6+
def test_quote_relation_name_once():
7+
"""
8+
Verify quoting a simple or full-qualified relation name.
9+
"""
10+
11+
# Table name only.
12+
assert quote_relation_name("my_table") == "my_table"
13+
assert quote_relation_name("my-table") == '"my-table"'
14+
assert quote_relation_name("MyTable") == '"MyTable"'
15+
assert quote_relation_name('"MyTable"') == '"MyTable"'
16+
17+
# Schema and table name.
18+
assert quote_relation_name("my_schema.my_table") == "my_schema.my_table"
19+
assert quote_relation_name("my-schema.my_table") == '"my-schema".my_table'
20+
assert quote_relation_name('"wrong-quoted-fqn.my_table"') == '"wrong-quoted-fqn.my_table"'
21+
assert quote_relation_name('"my_schema"."my_table"') == '"my_schema"."my_table"'
22+
23+
# Catalog, schema, and table name.
24+
assert quote_relation_name("crate.doc.t01") == "crate.doc.t01"
25+
26+
27+
def test_quote_relation_name_twice():
28+
"""
29+
Verify quoting a relation name twice does not cause any harm.
30+
"""
31+
input_fqn = "foo-bar.baz_qux"
32+
output_fqn = '"foo-bar".baz_qux'
33+
assert quote_relation_name(input_fqn) == output_fqn
34+
assert quote_relation_name(output_fqn) == output_fqn
35+
36+
37+
def test_quote_relation_name_reserved_keywords():
38+
"""
39+
Verify quoting a simple relation name that is a reserved keyword.
40+
"""
41+
assert quote_relation_name("table") == '"table"'
42+
assert quote_relation_name("true") == '"true"'
43+
assert quote_relation_name("select") == '"select"'
44+
45+
46+
def test_quote_relation_name_with_invalid_fqn():
47+
"""
48+
Verify quoting a relation name with an invalid fqn raises an error.
49+
"""
50+
with pytest.raises(ValueError):
51+
quote_relation_name("too-many.my-db.my-schema.my-table")

0 commit comments

Comments
 (0)