Skip to content

Commit 640a2db

Browse files
committed
SQLAlchemy: Fix SQL statement caching for CrateDB's OBJECT type
The SQLAlchemy implementation of CrateDB's `OBJECT` type offers indexed access to the instance's content in form of a dictionary. Thus, it must not use `cache_ok = True` on its implementation, i.e. this part of the compiled SQL clause must not be cached. Tests: Add integration test cases verifying SA's SQL statement caching Specifically, the special types `OBJECT` and `ARRAY` are of concern here.
1 parent 4e20913 commit 640a2db

File tree

5 files changed

+132
-4
lines changed

5 files changed

+132
-4
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Unreleased
1313

1414
- Allow handling datetime values tagged with time zone info when inserting or updating.
1515

16+
- SQLAlchemy: Fix SQL statement caching for CrateDB's ``OBJECT`` type.
17+
1618

1719
2023/04/18 0.31.1
1820
=================

src/crate/client/sqlalchemy/tests/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
from .dialect_test import SqlAlchemyDialectTest
2424
from .function_test import SqlAlchemyFunctionTest
2525
from .warnings_test import SqlAlchemyWarningsTest
26+
from .query_caching import SqlAlchemyQueryCompilationCaching
2627

2728

28-
def test_suite():
29+
def test_suite_unit():
2930
tests = TestSuite()
3031
tests.addTest(makeSuite(SqlAlchemyConnectionTest))
3132
tests.addTest(makeSuite(SqlAlchemyDictTypeTest))
@@ -42,3 +43,9 @@ def test_suite():
4243
tests.addTest(makeSuite(SqlAlchemyArrayTypeTest))
4344
tests.addTest(makeSuite(SqlAlchemyWarningsTest))
4445
return tests
46+
47+
48+
def test_suite_integration():
49+
tests = TestSuite()
50+
tests.addTest(makeSuite(SqlAlchemyQueryCompilationCaching))
51+
return tests
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# -*- coding: utf-8; -*-
2+
#
3+
# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
4+
# license agreements. See the NOTICE file distributed with this work for
5+
# additional information regarding copyright ownership. Crate licenses
6+
# this file to you under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License. You may
8+
# obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15+
# License for the specific language governing permissions and limitations
16+
# under the License.
17+
#
18+
# However, if you have executed another commercial license agreement
19+
# with Crate these terms will supersede the license and you may use the
20+
# software solely pursuant to the terms of the relevant commercial agreement.
21+
22+
from __future__ import absolute_import
23+
from unittest import TestCase, skipIf
24+
25+
import sqlalchemy as sa
26+
from sqlalchemy.orm import Session
27+
from sqlalchemy.sql.operators import eq
28+
29+
from crate.client.sqlalchemy import SA_VERSION, SA_1_4
30+
from crate.testing.settings import crate_host
31+
32+
try:
33+
from sqlalchemy.orm import declarative_base
34+
except ImportError:
35+
from sqlalchemy.ext.declarative import declarative_base
36+
37+
from crate.client.sqlalchemy.types import Object, ObjectArray
38+
39+
40+
class SqlAlchemyQueryCompilationCaching(TestCase):
41+
42+
def setUp(self):
43+
self.engine = sa.create_engine(f"crate://{crate_host}")
44+
self.metadata = sa.MetaData(schema="testdrive")
45+
self.session = Session(bind=self.engine)
46+
self.Character = self.setup_entity()
47+
48+
def setup_entity(self):
49+
"""
50+
Define ORM entity.
51+
"""
52+
Base = declarative_base(metadata=self.metadata)
53+
54+
class Character(Base):
55+
__tablename__ = 'characters'
56+
name = sa.Column(sa.String, primary_key=True)
57+
age = sa.Column(sa.Integer)
58+
data = sa.Column(Object)
59+
data_list = sa.Column(ObjectArray)
60+
61+
return Character
62+
63+
def setup_data(self):
64+
"""
65+
Insert two records into the `characters` table.
66+
"""
67+
self.metadata.drop_all(self.engine)
68+
self.metadata.create_all(self.engine)
69+
70+
Character = self.Character
71+
char1 = Character(name='Trillian', data={'x': 1}, data_list=[{'foo': 1, 'bar': 10}])
72+
char2 = Character(name='Slartibartfast', data={'y': 2}, data_list=[{'bar': 2}])
73+
self.session.add(char1)
74+
self.session.add(char2)
75+
self.session.commit()
76+
self.session.execute(sa.text("REFRESH TABLE testdrive.characters;"))
77+
78+
@skipIf(SA_VERSION < SA_1_4, "On SA13, the 'ResultProxy' object has no attribute 'scalar_one'")
79+
def test_object_multiple_select(self):
80+
"""
81+
The SQLAlchemy implementation of CrateDB's `OBJECT` type offers indexed
82+
access to the instance's content in form of a dictionary. Thus, it must
83+
not use `cache_ok = True` on its implementation, i.e. this part of the
84+
compiled SQL clause must not be cached.
85+
86+
This test verifies that two subsequent `SELECT` statements are translated
87+
well, and don't trip on incorrect SQL compiled statement caching.
88+
"""
89+
self.setup_data()
90+
Character = self.Character
91+
92+
selectable = sa.select(Character).where(Character.data['x'] == 1)
93+
result = self.session.execute(selectable).scalar_one().data
94+
self.assertEqual({"x": 1}, result)
95+
96+
selectable = sa.select(Character).where(Character.data['y'] == 2)
97+
result = self.session.execute(selectable).scalar_one().data
98+
self.assertEqual({"y": 2}, result)
99+
100+
@skipIf(SA_VERSION < SA_1_4, "On SA13, the 'ResultProxy' object has no attribute 'scalar_one'")
101+
def test_objectarray_multiple_select(self):
102+
"""
103+
The SQLAlchemy implementation of CrateDB's `ARRAY` type in form of the
104+
`ObjectArray`, does *not* offer indexed access to the instance's content.
105+
Thus, using `cache_ok = True` on that type should be sane, and not mess
106+
up SQLAlchemy's SQL compiled statement caching.
107+
"""
108+
self.setup_data()
109+
Character = self.Character
110+
111+
selectable = sa.select(Character).where(Character.data_list['foo'].any(1, operator=eq))
112+
result = self.session.execute(selectable).scalar_one().data
113+
self.assertEqual({"x": 1}, result)
114+
115+
selectable = sa.select(Character).where(Character.data_list['bar'].any(2, operator=eq))
116+
result = self.session.execute(selectable).scalar_one().data
117+
self.assertEqual({"y": 2}, result)

src/crate/client/sqlalchemy/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def __eq__(self, other):
132132

133133

134134
class _Craty(sqltypes.UserDefinedType):
135-
cache_ok = True
135+
cache_ok = False
136136

137137
class Comparator(sqltypes.TypeEngine.Comparator):
138138

src/crate/client/tests.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
TestCrateJsonEncoder,
5555
TestDefaultSchemaHeader,
5656
)
57-
from .sqlalchemy.tests import test_suite as sqlalchemy_test_suite
57+
from .sqlalchemy.tests import test_suite_unit as sqlalchemy_test_suite_unit
58+
from .sqlalchemy.tests import test_suite_integration as sqlalchemy_test_suite_integration
5859

5960
log = logging.getLogger('crate.testing.layer')
6061
ch = logging.StreamHandler()
@@ -344,7 +345,7 @@ def test_suite():
344345
suite.addTest(unittest.makeSuite(TestUsernameSentAsHeader))
345346
suite.addTest(unittest.makeSuite(TestCrateJsonEncoder))
346347
suite.addTest(unittest.makeSuite(TestDefaultSchemaHeader))
347-
suite.addTest(sqlalchemy_test_suite())
348+
suite.addTest(sqlalchemy_test_suite_unit())
348349
suite.addTest(doctest.DocTestSuite('crate.client.connection'))
349350
suite.addTest(doctest.DocTestSuite('crate.client.http'))
350351

@@ -394,6 +395,7 @@ def test_suite():
394395
encoding='utf-8'
395396
)
396397
s.layer = ensure_cratedb_layer()
398+
s.addTest(sqlalchemy_test_suite_integration())
397399
suite.addTest(s)
398400

399401
return suite

0 commit comments

Comments
 (0)