prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <issue>
  Get filtered set of records from table
  **Problem**
  <!-- Please provide a clear and concise description of the problem that this feature request is designed to solve.-->
  
  At the moment, when we request records from a table, it's _all_ of the records or none.  We should be able to filter a way that supports retrieving "groups" of records, where a group is defined as a set of records where some subset of the columns match a set of values, equivalent to the SQL:
  
  ```sql
  WHERE col1 = val1 AND col2 = val2 AND ... AND coln = valn
  ```
  
  **Proposed solution**
  <!-- A clear and concise description of your proposed solution or feature. -->
  
  We should create a function that lets us filter string columns using the pattern above, and another function that retrieves the distinct tuples for a set of columns (i.e., the groups in this context).
  
  **Additional context**
  <!-- Add any other context or screenshots about the feature request here.-->
  
  The interesting bit will be figuring out how to paginate the results, but without having to reperform the (costly) filtering query each time.
  
  </issue>
  <code>
  [start of db/records.py]
  1 from sqlalchemy import delete, select
  2 from sqlalchemy.inspection import inspect
  3 
  4 
  5 def _get_primary_key_column(table):
  6     primary_key_list = list(inspect(table).primary_key)
  7     # We do not support getting by composite primary keys
  8     assert len(primary_key_list) == 1
  9     return primary_key_list[0]
  10 
  11 
  12 def get_record(table, engine, id_value):
  13     primary_key_column = _get_primary_key_column(table)
  14     query = select(table).where(primary_key_column == id_value)
  15     with engine.begin() as conn:
  16         result = conn.execute(query).fetchall()
  17         assert len(result) <= 1
  18         return result[0] if result else None
  19 
  20 
  21 def get_records(table, engine, limit=None, offset=None, order_by=[]):
  22     """
  23     Returns records from a table.
  24 
  25     Args:
  26         table:    SQLAlchemy table object
  27         engine:   SQLAlchemy engine object
  28         limit:    int, gives number of rows to return
  29         offset:   int, gives number of rows to skip
  30         order_by: list of SQLAlchemy ColumnElements to order by.  Should
  31                   usually be either a list of string column names, or a
  32                   list of columns from the given table.
  33     """
  34     query = select(table).order_by(*order_by).limit(limit).offset(offset)
  35     with engine.begin() as conn:
  36         return conn.execute(query).fetchall()
  37 
  38 
  39 def create_record_or_records(table, engine, record_data):
  40     """
  41     record_data can be a dictionary, tuple, or list of dictionaries or tuples.
  42     if record_data is a list, it creates multiple records.
  43     """
  44     id_value = None
  45     with engine.begin() as connection:
  46         result = connection.execute(table.insert(), record_data)
  47         # If there was only a single record created, return the record.
  48         if result.rowcount == 1:
  49             # We need to manually commit insertion so that we can retrieve the record.
  50             connection.commit()
  51             id_value = result.inserted_primary_key[0]
  52             if id_value is not None:
  53                 return get_record(table, engine, id_value)
  54     # Do not return any records if multiple rows were added.
  55     return None
  56 
  57 
  58 def create_records_from_csv(table, engine, csv_filename, column_names):
  59     with open(csv_filename, 'rb') as csv_file:
  60         with engine.begin() as conn:
  61             cursor = conn.connection.cursor()
  62             relation = '.'.join('"{}"'.format(part) for part in (table.schema, table.name))
  63             formatted_columns = '({})'.format(','.join([f'"{column_name}"' for column_name in column_names]))
  64             copy_sql = f'COPY {relation} {formatted_columns} FROM STDIN CSV HEADER'
  65             cursor.copy_expert(copy_sql, csv_file)
  66 
  67 
  68 def update_record(table, engine, id_value, record_data):
  69     primary_key_column = _get_primary_key_column(table)
  70     with engine.begin() as connection:
  71         connection.execute(
  72             table.update().where(primary_key_column == id_value).values(record_data)
  73         )
  74     return get_record(table, engine, id_value)
  75 
  76 
  77 def delete_record(table, engine, id_value):
  78     primary_key_column = _get_primary_key_column(table)
  79     query = delete(table).where(primary_key_column == id_value)
  80     with engine.begin() as conn:
  81         return conn.execute(query)
  82
  [end of db/records.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: diff --git a/db/records.py b/db/records.py
  --- a/db/records.py
  +++ b/db/records.py
  @@ -1,6 +1,9 @@
  -from sqlalchemy import delete, select
  +import logging
  +from sqlalchemy import delete, select, and_, Column
   from sqlalchemy.inspection import inspect
   
  +logger = logging.getLogger(__name__)
  +
   
   def _get_primary_key_column(table):
       primary_key_list = list(inspect(table).primary_key)
  @@ -18,7 +21,9 @@
           return result[0] if result else None
   
   
  -def get_records(table, engine, limit=None, offset=None, order_by=[]):
  +def get_records(
  +        table, engine, limit=None, offset=None, order_by=[], filters=[]
  +):
       """
       Returns records from a table.
   
  @@ -30,12 +35,72 @@
           order_by: list of SQLAlchemy ColumnElements to order by.  Should
                     usually be either a list of string column names, or a
                     list of columns from the given table.
  +        filters:  list of tuples of type (ColumnElement, value), where
  +                  ColumnElement is an SQLAlchemy ColumnElement, and value
  +                  is a valid value for the associated column (i.e., the
  +                  type must be correct)
       """
  -    query = select(table).order_by(*order_by).limit(limit).offset(offset)
  +    query = (
  +        select(table)
  +        .order_by(*order_by)
  +        .limit(limit)
  +        .offset(offset)
  +        .where(_build_filter_conjunction(table, filters))
  +    )
       with engine.begin() as conn:
           return conn.execute(query).fetchall()
   
   
  +def _build_filter_conjunction(table, filters):
  +    refined_filters = [
  +        (table.columns[col] if type(col) == str else col, value)
  +        for col, value in filters
  +    ]
  +    # We need a default of True (rather than empty), since invoking and_
  +    # without arguments is deprecated.
  +    return and_(True, *[col == value for col, value in refined_filters])
  +
  +
  +def get_distinct_tuple_values(
  +        column_list, engine, table=None, limit=None, offset=None,
  +):
  +    """
  +    Returns distinct tuples from a given list of columns.
  +
  +    Args:
  +        column_list: list of column names or SQLAlchemy column objects
  +        engine:   SQLAlchemy engine object
  +        table:    SQLAlchemy table object
  +        limit:    int, gives number of rows to return
  +        offset:   int, gives number of rows to skip
  +
  +    If no table is given, the column_list must consist entirely of
  +    SQLAlchemy column objects associated with a table.
  +    """
  +    if table is not None:
  +        column_objects = [
  +            table.columns[col] if type(col) == str else col
  +            for col in column_list
  +        ]
  +    else:
  +        column_objects = column_list
  +    try:
  +        assert all([type(col) == Column for col in column_objects])
  +    except AssertionError as e:
  +        logger.error("All columns must be str or sqlalchemy.Column type")
  +        raise e
  +
  +    query = (
  +        select(*column_objects)
  +        .distinct()
  +        .limit(limit)
  +        .offset(offset)
  +    )
  +    with engine.begin() as conn:
  +        res = conn.execute(query).fetchall()
  +    return [tuple(zip(column_objects, row)) for row in res]
  +
  +
   def create_record_or_records(table, engine, record_data):
       """
       record_data can be a dictionary, tuple, or list of dictionaries or tuples.
  
verification_info: {"golden_diff": "diff --git a/db/records.py b/db/records.py\n--- a/db/records.py\n+++ b/db/records.py\n@@ -1,6 +1,9 @@\n-from sqlalchemy import delete, select\n+import logging\n+from sqlalchemy import delete, select, and_, Column\n from sqlalchemy.inspection import inspect\n \n+logger = logging.getLogger(__name__)\n+\n \n def _get_primary_key_column(table):\n     primary_key_list = list(inspect(table).primary_key)\n@@ -18,7 +21,9 @@\n         return result[0] if result else None\n \n \n-def get_records(table, engine, limit=None, offset=None, order_by=[]):\n+def get_records(\n+        table, engine, limit=None, offset=None, order_by=[], filters=[]\n+):\n     \"\"\"\n     Returns records from a table.\n \n@@ -30,12 +35,72 @@\n         order_by: list of SQLAlchemy ColumnElements to order by.  Should\n                   usually be either a list of string column names, or a\n                   list of columns from the given table.\n+        filters:  list of tuples of type (ColumnElement, value), where\n+                  ColumnElement is an SQLAlchemy ColumnElement, and value\n+                  is a valid value for the associated column (i.e., the\n+                  type must be correct)\n     \"\"\"\n-    query = select(table).order_by(*order_by).limit(limit).offset(offset)\n+    query = (\n+        select(table)\n+        .order_by(*order_by)\n+        .limit(limit)\n+        .offset(offset)\n+        .where(_build_filter_conjunction(table, filters))\n+    )\n     with engine.begin() as conn:\n         return conn.execute(query).fetchall()\n \n \n+def _build_filter_conjunction(table, filters):\n+    refined_filters = [\n+        (table.columns[col] if type(col) == str else col, value)\n+        for col, value in filters\n+    ]\n+    # We need a default of True (rather than empty), since invoking and_\n+    # without arguments is deprecated.\n+    return and_(True, *[col == value for col, value in refined_filters])\n+\n+\n+def get_distinct_tuple_values(\n+        column_list, engine, table=None, limit=None, offset=None,\n+):\n+    \"\"\"\n+    Returns distinct tuples from a given list of columns.\n+\n+    Args:\n+        column_list: list of column names or SQLAlchemy column objects\n+        engine:   SQLAlchemy engine object\n+        table:    SQLAlchemy table object\n+        limit:    int, gives number of rows to return\n+        offset:   int, gives number of rows to skip\n+\n+    If no table is given, the column_list must consist entirely of\n+    SQLAlchemy column objects associated with a table.\n+    \"\"\"\n+    if table is not None:\n+        column_objects = [\n+            table.columns[col] if type(col) == str else col\n+            for col in column_list\n+        ]\n+    else:\n+        column_objects = column_list\n+    try:\n+        assert all([type(col) == Column for col in column_objects])\n+    except AssertionError as e:\n+        logger.error(\"All columns must be str or sqlalchemy.Column type\")\n+        raise e\n+\n+    query = (\n+        select(*column_objects)\n+        .distinct()\n+        .limit(limit)\n+        .offset(offset)\n+    )\n+    with engine.begin() as conn:\n+        res = conn.execute(query).fetchall()\n+    return [tuple(zip(column_objects, row)) for row in res]\n+\n+\n def create_record_or_records(table, engine, record_data):\n     \"\"\"\n     record_data can be a dictionary, tuple, or list of dictionaries or tuples.\n", "issue": "Get filtered set of records from table\n**Problem**\r\n<!-- Please provide a clear and concise description of the problem that this feature request is designed to solve.-->\r\n\r\nAt the moment, when we request records from a table, it's _all_ of the records or none.  We should be able to filter a way that supports retrieving \"groups\" of records, where a group is defined as a set of records where some subset of the columns match a set of values, equivalent to the SQL:\r\n\r\n```sql\r\nWHERE col1 = val1 AND col2 = val2 AND ... AND coln = valn\r\n```\r\n\r\n**Proposed solution**\r\n<!-- A clear and concise description of your proposed solution or feature. -->\r\n\r\nWe should create a function that lets us filter string columns using the pattern above, and another function that retrieves the distinct tuples for a set of columns (i.e., the groups in this context).\r\n\r\n**Additional context**\r\n<!-- Add any other context or screenshots about the feature request here.-->\r\n\r\nThe interesting bit will be figuring out how to paginate the results, but without having to reperform the (costly) filtering query each time.\n", "before_files": [{"content": "from sqlalchemy import delete, select\nfrom sqlalchemy.inspection import inspect\n\n\ndef _get_primary_key_column(table):\n    primary_key_list = list(inspect(table).primary_key)\n    # We do not support getting by composite primary keys\n    assert len(primary_key_list) == 1\n    return primary_key_list[0]\n\n\ndef get_record(table, engine, id_value):\n    primary_key_column = _get_primary_key_column(table)\n    query = select(table).where(primary_key_column == id_value)\n    with engine.begin() as conn:\n        result = conn.execute(query).fetchall()\n        assert len(result) <= 1\n        return result[0] if result else None\n\n\ndef get_records(table, engine, limit=None, offset=None, order_by=[]):\n    \"\"\"\n    Returns records from a table.\n\n    Args:\n        table:    SQLAlchemy table object\n        engine:   SQLAlchemy engine object\n        limit:    int, gives number of rows to return\n        offset:   int, gives number of rows to skip\n        order_by: list of SQLAlchemy ColumnElements to order by.  Should\n                  usually be either a list of string column names, or a\n                  list of columns from the given table.\n    \"\"\"\n    query = select(table).order_by(*order_by).limit(limit).offset(offset)\n    with engine.begin() as conn:\n        return conn.execute(query).fetchall()\n\n\ndef create_record_or_records(table, engine, record_data):\n    \"\"\"\n    record_data can be a dictionary, tuple, or list of dictionaries or tuples.\n    if record_data is a list, it creates multiple records.\n    \"\"\"\n    id_value = None\n    with engine.begin() as connection:\n        result = connection.execute(table.insert(), record_data)\n        # If there was only a single record created, return the record.\n        if result.rowcount == 1:\n            # We need to manually commit insertion so that we can retrieve the record.\n            connection.commit()\n            id_value = result.inserted_primary_key[0]\n            if id_value is not None:\n                return get_record(table, engine, id_value)\n    # Do not return any records if multiple rows were added.\n    return None\n\n\ndef create_records_from_csv(table, engine, csv_filename, column_names):\n    with open(csv_filename, 'rb') as csv_file:\n        with engine.begin() as conn:\n            cursor = conn.connection.cursor()\n            relation = '.'.join('\"{}\"'.format(part) for part in (table.schema, table.name))\n            formatted_columns = '({})'.format(','.join([f'\"{column_name}\"' for column_name in column_names]))\n            copy_sql = f'COPY {relation} {formatted_columns} FROM STDIN CSV HEADER'\n            cursor.copy_expert(copy_sql, csv_file)\n\n\ndef update_record(table, engine, id_value, record_data):\n    primary_key_column = _get_primary_key_column(table)\n    with engine.begin() as connection:\n        connection.execute(\n            table.update().where(primary_key_column == id_value).values(record_data)\n        )\n    return get_record(table, engine, id_value)\n\n\ndef delete_record(table, engine, id_value):\n    primary_key_column = _get_primary_key_column(table)\n    query = delete(table).where(primary_key_column == id_value)\n    with engine.begin() as conn:\n        return conn.execute(query)\n", "path": "db/records.py"}]}