Skip to content

Commit 71d787f

Browse files
stsnelalanking
authored andcommitted
[#535] Implement basic support for GenQuery2
1 parent a10bdbb commit 71d787f

File tree

6 files changed

+199
-0
lines changed

6 files changed

+199
-0
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Currently supported:
1111
- iRODS connection over SSL
1212
- Implement basic GenQueries (select columns and filtering)
1313
- Support more advanced GenQueries with limits, offsets, and aggregations
14+
- Support for queries using the GenQuery2 interface
1415
- Query the collections and data objects within a collection
1516
- Execute direct SQL queries
1617
- Execute iRODS rules
@@ -1252,6 +1253,33 @@ As stated, this type of object discovery requires some extra study and
12521253
effort, but the ability to search arbitrary iRODS zones (to which we are
12531254
federated and have the user permissions) is powerful indeed.
12541255

1256+
1257+
GenQuery2 queries
1258+
-------
1259+
1260+
GenQuery2 is a successor to the regular GenQuery interface. It is available
1261+
by default on iRODS 4.3.2 and higher. GenQuery2 currently has an experimental status,
1262+
and is subject to change.
1263+
1264+
Queries can be executed using the `genquery2` function. For example:
1265+
1266+
```
1267+
>>> session.genquery2("SELECT COLL_NAME WHERE COLL_NAME = '/tempZone/home' OR COLL_NAME LIKE '%/genquery2_dummy_doesnotexist'")
1268+
[['/tempZone/home']]
1269+
```
1270+
1271+
Alternatively, create a GenQuery2 object and use it to execute queries. For example:
1272+
1273+
```
1274+
>>> q = session.genquery2_object()
1275+
>>> q.execute("SELECT COLL_NAME WHERE COLL_NAME = '/tempZone/home' OR COLL_NAME LIKE '%/genquery2_dummy_doesnotexist'", zone="tempZone")
1276+
[['/tempZone/home']]
1277+
```
1278+
1279+
GenQuery2 objects also support retrieving the SQL generated by a GenQuery2 query using the
1280+
`get_sql` function and retrieving column mappings using the `get_column_mappings` function.
1281+
1282+
12551283
Tickets
12561284
-------
12571285

irods/api_number.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@
178178
"SSL_END_AN": 1101,
179179
"CLIENT_HINTS_AN": 10215,
180180
"GET_RESOURCE_INFO_FOR_OPERATION_AN": 10220,
181+
"GENQUERY2_AN": 10221,
181182
"ATOMIC_APPLY_METADATA_OPERATIONS_APN": 20002,
182183
"GET_FILE_DESCRIPTOR_INFO_APN": 20000,
183184
"REPLICA_CLOSE_APN": 20004,

irods/genquery2.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import json
2+
3+
from irods.api_number import api_number
4+
from irods.exception import OperationNotSupported
5+
from irods.message import GenQuery2Request, STR_PI, iRODSMessage
6+
7+
8+
class GenQuery2(object):
9+
"""Interface to the GenQuery2 API
10+
11+
This class provides an interface to the GenQuery2 API, an experimental
12+
iRODS API for querying iRODS. GenQuery2 is an improved version of the
13+
traditional GenQuery interface. The GenQuery2 interface may be subject
14+
to change.
15+
"""
16+
17+
def __init__(self, session):
18+
self.session = session
19+
if not self._is_supported():
20+
raise OperationNotSupported(
21+
"GenQuery2 is not supported by default on this iRODS version.")
22+
23+
def execute(self, query, zone=None):
24+
"""Execute this GenQuery2 query, and return the results."""
25+
effective_zone = self.session.zone if zone is None else zone
26+
return json.loads(self._exec_genquery2(query, effective_zone))
27+
28+
def get_sql(self, query, zone=None):
29+
"""Return the SQL query that this GenQuery2 query will be translated to."""
30+
effective_zone = self.session.zone if zone is None else zone
31+
return self._exec_genquery2(query, effective_zone, sql_flag=True)
32+
33+
def get_column_mappings(self, zone=None):
34+
effective_zone = self.session.zone if zone is None else zone
35+
return json.loads(self._exec_genquery2(
36+
"", effective_zone, column_mappings_flag=True))
37+
38+
def _exec_genquery2(self, query, zone, sql_flag=False,
39+
column_mappings_flag=False):
40+
msg = GenQuery2Request()
41+
msg.query_string = query
42+
msg.zone = zone
43+
msg.sql_only = 1 if sql_flag else 0
44+
msg.column_mappings = 1 if column_mappings_flag else 0
45+
message = iRODSMessage('RODS_API_REQ',
46+
msg=msg,
47+
int_info=api_number['GENQUERY2_AN'])
48+
with self.session.pool.get_connection() as conn:
49+
conn.send(message)
50+
response = conn.recv()
51+
return response.get_main_message(STR_PI).myStr
52+
53+
def _is_supported(self):
54+
"""Checks whether this iRODS server supports GenQuery2."""
55+
return self.session.server_version >= (4, 3, 2)

irods/message/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,12 @@ class GenQueryResponse(Message):
628628
# openFlags; double offset; double dataSize; int numThreads; int oprType;
629629
# struct *SpecColl_PI; struct KeyValPair_PI;"
630630

631+
class GenQuery2Request(Message):
632+
_name = 'Genquery2Input_PI'
633+
query_string = StringProperty()
634+
zone = StringProperty()
635+
sql_only = IntegerProperty()
636+
column_mappings = IntegerProperty()
631637

632638
class FileOpenRequest(Message):
633639
_name = 'DataObjInp_PI'

irods/session.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import threading
1010
import weakref
1111
from irods.query import Query
12+
from irods.genquery2 import GenQuery2
1213
from irods.pool import Pool
1314
from irods.account import iRODSAccount
1415
from irods.api_number import api_number
@@ -269,6 +270,20 @@ def configure(self, **kwargs):
269270
def query(self, *args, **kwargs):
270271
return Query(self, *args, **kwargs)
271272

273+
def genquery2_object(self, **kwargs):
274+
""" Returns GenQuery2 object
275+
276+
Returns GenQuery2 object that can be used to execute GenQuery2 queries,
277+
to retrieve the SQL query for a particular GenQuery2 query, and to
278+
get GenQuery2 column mappings.
279+
"""
280+
return GenQuery2(self, **kwargs)
281+
282+
def genquery2(self, query, **kwargs):
283+
"""Shorthand for executing a single GenQuery2 query."""
284+
q = GenQuery2(self)
285+
return q.execute(query, **kwargs)
286+
272287
@property
273288
def username(self):
274289
return self.pool.account.client_user

irods/test/genquery2_test.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import unittest
2+
3+
import irods.test.helpers as helpers
4+
5+
6+
class TestGenQuery2(unittest.TestCase):
7+
8+
def setUp(self):
9+
self.sess = helpers.make_session()
10+
11+
if self.sess.server_version < (4, 3, 2):
12+
self.skipTest(
13+
'GenQuery2 is not available by default in iRODS before v4.3.2.')
14+
15+
self.coll_path_a = '/{}/home/{}/test_query2_coll_a'.format(
16+
self.sess.zone, self.sess.username)
17+
self.coll_path_b = '/{}/home/{}/test_query2_coll_b'.format(
18+
self.sess.zone, self.sess.username)
19+
self.sess.collections.create(self.coll_path_a)
20+
self.sess.collections.create(self.coll_path_b)
21+
22+
def tearDown(self):
23+
'''Remove test data and close connections
24+
'''
25+
self.sess.collections.remove(self.coll_path_a, force=True)
26+
self.sess.collections.remove(self.coll_path_b, force=True)
27+
self.sess.cleanup()
28+
29+
def test_select(self):
30+
query = "SELECT COLL_NAME WHERE COLL_NAME = '{}'".format(
31+
self.coll_path_a)
32+
q = self.sess.genquery2_object()
33+
query_result = q.execute(query)
34+
query_sql = q.get_sql(query)
35+
self.assertIn([self.coll_path_a], query_result)
36+
self.assertEqual(len(query_result), 1)
37+
# This assumes the iCAT database runs on PostgreSQL
38+
self.assertEqual(query_sql, "select distinct t0.coll_name from R_COLL_MAIN t0 inner join R_OBJT_ACCESS pcoa on t0.coll_id = pcoa.object_id inner join R_TOKN_MAIN pct on pcoa.access_type_id = pct.token_id inner join R_USER_MAIN pcu on pcoa.user_id = pcu.user_id where t0.coll_name = ? and pcoa.access_type_id >= 1000 fetch first 256 rows only")
39+
40+
def test_select_with_explicit_zone(self):
41+
query = "SELECT COLL_NAME WHERE COLL_NAME = '{}'".format(
42+
self.coll_path_a)
43+
q = self.sess.genquery2_object()
44+
query_result = q.execute(query, zone=self.sess.zone)
45+
query_sql = q.get_sql(query, zone=self.sess.zone)
46+
self.assertIn([self.coll_path_a], query_result)
47+
self.assertEqual(len(query_result), 1)
48+
# This assumes the iCAT database runs on PostgreSQL
49+
self.assertEqual(query_sql, "select distinct t0.coll_name from R_COLL_MAIN t0 inner join R_OBJT_ACCESS pcoa on t0.coll_id = pcoa.object_id inner join R_TOKN_MAIN pct on pcoa.access_type_id = pct.token_id inner join R_USER_MAIN pcu on pcoa.user_id = pcu.user_id where t0.coll_name = ? and pcoa.access_type_id >= 1000 fetch first 256 rows only")
50+
51+
def test_select_with_shorthand(self):
52+
query = "SELECT COLL_NAME WHERE COLL_NAME = '{}'".format(
53+
self.coll_path_a)
54+
query_result = self.sess.genquery2(query)
55+
self.assertIn([self.coll_path_a], query_result)
56+
self.assertEqual(len(query_result), 1)
57+
58+
def test_select_with_shorthand_and_explicit_zone(self):
59+
query = "SELECT COLL_NAME WHERE COLL_NAME = '{}'".format(
60+
self.coll_path_a)
61+
query_result = self.sess.genquery2(query, zone=self.sess.zone)
62+
self.assertIn([self.coll_path_a], query_result)
63+
self.assertEqual(len(query_result), 1)
64+
65+
def test_select_or(self):
66+
query = "SELECT COLL_NAME WHERE COLL_NAME = '{}' OR COLL_NAME = '{}'".format(
67+
self.coll_path_a, self.coll_path_b)
68+
q = self.sess.genquery2_object()
69+
query_result = q.execute(query)
70+
query_sql = q.get_sql(query)
71+
self.assertIn([self.coll_path_a], query_result)
72+
self.assertIn([self.coll_path_b], query_result)
73+
self.assertEqual(len(query_result), 2)
74+
# This assumes the iCAT database runs on PostgreSQL
75+
self.assertEqual(query_sql, "select distinct t0.coll_name from R_COLL_MAIN t0 inner join R_OBJT_ACCESS pcoa on t0.coll_id = pcoa.object_id inner join R_TOKN_MAIN pct on pcoa.access_type_id = pct.token_id inner join R_USER_MAIN pcu on pcoa.user_id = pcu.user_id where t0.coll_name = ? or t0.coll_name = ? and pcoa.access_type_id >= 1000 fetch first 256 rows only")
76+
77+
def test_select_and(self):
78+
query = "SELECT COLL_NAME WHERE COLL_NAME LIKE '{}' AND COLL_NAME LIKE '{}'".format(
79+
"%test_query2_coll%", "%query2_coll_a%")
80+
q = self.sess.genquery2_object()
81+
query_result = q.execute(query)
82+
query_sql = q.get_sql(query)
83+
self.assertIn([self.coll_path_a], query_result)
84+
self.assertEqual(len(query_result), 1)
85+
# This assumes the iCAT database runs on PostgreSQL
86+
self.assertEqual(query_sql, "select distinct t0.coll_name from R_COLL_MAIN t0 inner join R_OBJT_ACCESS pcoa on t0.coll_id = pcoa.object_id inner join R_TOKN_MAIN pct on pcoa.access_type_id = pct.token_id inner join R_USER_MAIN pcu on pcoa.user_id = pcu.user_id where t0.coll_name like ? and t0.coll_name like ? and pcoa.access_type_id >= 1000 fetch first 256 rows only")
87+
88+
def test_column_mappings(self):
89+
q = self.sess.genquery2_object()
90+
result = q.get_column_mappings()
91+
self.assertIn("COLL_ID", result.keys())
92+
self.assertIn("DATA_ID", result.keys())
93+
self.assertIn("RESC_ID", result.keys())
94+
self.assertIn("USER_ID", result.keys())

0 commit comments

Comments
 (0)