Skip to content

Commit

Permalink
[535] Implement basic support for GenQuery2
Browse files Browse the repository at this point in the history
  • Loading branch information
stsnel committed May 13, 2024
1 parent 86a8f11 commit ff51694
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 0 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Currently supported:
- iRODS connection over SSL
- Implement basic GenQueries (select columns and filtering)
- Support more advanced GenQueries with limits, offsets, and aggregations
- Support for queries using the GenQuery2 interface
- Query the collections and data objects within a collection
- Execute direct SQL queries
- Execute iRODS rules
Expand Down Expand Up @@ -1252,6 +1253,23 @@ As stated, this type of object discovery requires some extra study and
effort, but the ability to search arbitrary iRODS zones (to which we are
federated and have the user permissions) is powerful indeed.


GenQuery2 queries
-------

GenQuery2 is a successor to the regular GenQuery interface. It is available
by default on iRODS 4.3.2 and higher.

In order to use it, create and execute a Query2 object:

```
>>> q = session.query2("tempZone", "SELECT COLL_NAME WHERE COLL_NAME = '/tempZone/home' OR COLL_NAME LIKE '%/query2_dummy_doesnotexist'")
>>> q.execute()
[['/tempZone/home']]
```

The first argument is the zone name, and the second argument is the GenQuery2 query.

Tickets
-------

Expand Down
1 change: 1 addition & 0 deletions irods/api_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
"SSL_END_AN": 1101,
"CLIENT_HINTS_AN": 10215,
"GET_RESOURCE_INFO_FOR_OPERATION_AN": 10220,
"GENQUERY2_AN": 10221,
"ATOMIC_APPLY_METADATA_OPERATIONS_APN": 20002,
"GET_FILE_DESCRIPTOR_INFO_APN": 20000,
"REPLICA_CLOSE_APN": 20004,
Expand Down
6 changes: 6 additions & 0 deletions irods/message/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,12 @@ class GenQueryResponse(Message):
# openFlags; double offset; double dataSize; int numThreads; int oprType;
# struct *SpecColl_PI; struct KeyValPair_PI;"

class GenQuery2Request(Message):
_name = 'Genquery2Input_PI'
query_string = StringProperty()
zone = StringProperty()
sql_only = IntegerProperty()
column_mappings = IntegerProperty()

class FileOpenRequest(Message):
_name = 'DataObjInp_PI'
Expand Down
49 changes: 49 additions & 0 deletions irods/query2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from collections import OrderedDict
import json

from irods.api_number import api_number
from irods.exception import OperationNotSupported
from irods.message import GenQuery2Request, iRODSMessage, STR_PI

"""This class provides an interface to GenQuery2 API, an experimental
iRODS API for querying iRODS. GenQuery2 is an improved version of the
traditional GenQuery interface.
"""


class Query2(object):

def __init__(self, session, zone, query, override_not_supported=False):
self.session = session
self.zone = zone
self.query = query
if not (self._is_supported() or override_not_supported):
raise OperationNotSupported(
"GenQuery2 is not supported by default on this iRODS version.")

def execute(self):
"""Execute this GenQuery2 query, and return the results."""
return self._exec_genquery2()

def get_sql(self):
"""Return the SQL query that this GenQuery2 query will be translated to."""
return self._exec_genquery2(sql_flag=True)

def _exec_genquery2(self, sql_flag=False):
msg = GenQuery2Request()
msg.query_string = self.query
msg.zone = self.zone
msg.sql_only = 1 if sql_flag else 0
msg.column_mappings = 0
message = iRODSMessage('RODS_API_REQ',
msg=msg,
int_info=api_number['GENQUERY2_AN'])
with self.session.pool.get_connection() as conn:
conn.send(message)
response = conn.recv()
result = response.get_main_message(STR_PI).myStr
return result if sql_flag else json.loads(result)

def _is_supported(self):
"""Checks whether this iRODS server supports GenQuery2."""
return self.session.server_version >= (4, 3, 2)
4 changes: 4 additions & 0 deletions irods/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import threading
import weakref
from irods.query import Query
from irods.query2 import Query2
from irods.pool import Pool
from irods.account import iRODSAccount
from irods.api_number import api_number
Expand Down Expand Up @@ -269,6 +270,9 @@ def configure(self, **kwargs):
def query(self, *args, **kwargs):
return Query(self, *args, **kwargs)

def query2(self, *args, **kwargs):
return Query2(self, *args, **kwargs)

@property
def username(self):
return self.pool.account.client_user
Expand Down
51 changes: 51 additions & 0 deletions irods/test/query2_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import unittest

import irods.test.helpers as helpers


class TestQuery2(unittest.TestCase):

def setUp(self):
self.sess = helpers.make_session()

if self.sess.server_version < (4, 3, 2):
self.skipTest(
'GenQuery2 is not available by default in iRODS before v4.3.2.')

self.coll_path_a = '/{}/home/{}/test_query2_coll_a'.format(
self.sess.zone, self.sess.username)
self.coll_path_b = '/{}/home/{}/test_query2_coll_b'.format(
self.sess.zone, self.sess.username)
self.sess.collections.create(self.coll_path_a)
self.sess.collections.create(self.coll_path_b)

def test_select(self):
query = "SELECT COLL_NAME WHERE COLL_NAME = '{}'".format(
self.coll_path_a)
q = self.sess.query2(self.sess.zone, query)
query_result = q.execute()
query_sql = q.get_sql()
self.assertIn([self.coll_path_a], query_result)
self.assertEqual(len(query_result), 1)
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")

def test_select_or(self):
query = "SELECT COLL_NAME WHERE COLL_NAME = '{}' OR COLL_NAME = '{}'".format(
self.coll_path_a, self.coll_path_b)
q = self.sess.query2(self.sess.zone, query)
query_result = q.execute()
query_sql = q.get_sql()
self.assertIn([self.coll_path_a], query_result)
self.assertIn([self.coll_path_b], query_result)
self.assertEqual(len(query_result), 2)
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")

def test_select_and(self):
query = "SELECT COLL_NAME WHERE COLL_NAME LIKE '{}' AND COLL_NAME LIKE '{}'".format(
"%test_query2_coll%", "%query2_coll_a%")
q = self.sess.query2(self.sess.zone, query)
query_result = q.execute()
query_sql = q.get_sql()
self.assertIn([self.coll_path_a], query_result)
self.assertEqual(len(query_result), 1)
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")

0 comments on commit ff51694

Please sign in to comment.