{"id":34083000,"url":"https://github.com/adobe/dy-sql","last_synced_at":"2025-12-14T12:32:35.556Z","repository":{"id":37085863,"uuid":"388273978","full_name":"adobe/dy-sql","owner":"adobe","description":null,"archived":false,"fork":false,"pushed_at":"2025-10-14T13:22:32.000Z","size":253,"stargazers_count":24,"open_issues_count":1,"forks_count":7,"subscribers_count":8,"default_branch":"main","last_synced_at":"2025-10-23T17:47:57.740Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adobe.png","metadata":{"files":{"readme":"README.rst","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2021-07-21T23:42:00.000Z","updated_at":"2025-10-14T13:21:30.000Z","dependencies_parsed_at":"2023-12-21T00:23:39.238Z","dependency_job_id":"1888b363-8b1a-49cb-a216-2d8a12250aff","html_url":"https://github.com/adobe/dy-sql","commit_stats":{"total_commits":115,"total_committers":5,"mean_commits":23.0,"dds":0.408695652173913,"last_synced_commit":"8fb7762ebe18dea463a136ee369475e19cc442d9"},"previous_names":[],"tags_count":28,"template":false,"template_full_name":null,"purl":"pkg:github/adobe/dy-sql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adobe%2Fdy-sql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adobe%2Fdy-sql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adobe%2Fdy-sql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adobe%2Fdy-sql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adobe","download_url":"https://codeload.github.com/adobe/dy-sql/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adobe%2Fdy-sql/sbom","scorecard":{"id":167241,"data":{"date":"2025-08-11","repo":{"name":"github.com/adobe/dy-sql","commit":"71847998df917ad7e0f8a262597385a3959045de"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":5.5,"checks":[{"name":"Code-Review","score":8,"reason":"Found 8/9 approved changesets -- score normalized to 8","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":5,"reason":"6 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 5","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/build.yaml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yaml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/adobe/dy-sql/build.yaml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yaml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/adobe/dy-sql/build.yaml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/build.yaml:35: update your workflow using https://app.stepsecurity.io/secureworkflow/adobe/dy-sql/build.yaml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yaml:47: update your workflow using https://app.stepsecurity.io/secureworkflow/adobe/dy-sql/build.yaml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yaml:67: update your workflow using https://app.stepsecurity.io/secureworkflow/adobe/dy-sql/build.yaml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yaml:78: update your workflow using https://app.stepsecurity.io/secureworkflow/adobe/dy-sql/build.yaml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yaml:83: update your workflow using https://app.stepsecurity.io/secureworkflow/adobe/dy-sql/build.yaml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/build.yaml:102: update your workflow using https://app.stepsecurity.io/secureworkflow/adobe/dy-sql/build.yaml/main?enable=pin","Warn: containerImage not pinned by hash: Dockerfile:3","Warn: pipCommand not pinned by hash: Dockerfile:10","Warn: pipCommand not pinned by hash: .github/workflows/build.yaml:88","Warn: pipCommand not pinned by hash: .github/workflows/build.yaml:89","Warn: pipCommand not pinned by hash: .github/workflows/build.yaml:90","Warn: pipCommand not pinned by hash: .github/workflows/build.yaml:101","Warn: pipCommand not pinned by hash: .github/workflows/build.yaml:27","Warn: pipCommand not pinned by hash: .github/workflows/build.yaml:28","Warn: pipCommand not pinned by hash: .github/workflows/build.yaml:29","Warn: pipCommand not pinned by hash: .github/workflows/build.yaml:53","Warn: pipCommand not pinned by hash: .github/workflows/build.yaml:54","Info:   0 out of   6 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   2 third-party GitHubAction dependencies pinned","Info:   0 out of   1 containerImage dependencies pinned","Info:   0 out of  10 pipCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Packaging","score":10,"reason":"packaging workflow detected","details":["Info: Project packages its releases by way of GitHub Actions.: .github/workflows/build.yaml:73"],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Security-Policy","score":10,"reason":"security policy file detected","details":["Info: security policy file detected: github.com/adobe/.github/.github/SECURITY.md:1","Info: Found linked content: github.com/adobe/.github/.github/SECURITY.md:1","Info: Found disclosure, vulnerability, and/or timelines in security policy: github.com/adobe/.github/.github/SECURITY.md:1","Info: Found text in security policy: github.com/adobe/.github/.github/SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Vulnerabilities","score":9,"reason":"1 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-mr82-8j83-vxmv"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 30 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-16T15:15:06.089Z","repository_id":37085863,"created_at":"2025-08-16T15:15:06.089Z","updated_at":"2025-08-16T15:15:06.089Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27728262,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-14T02:00:11.348Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-12-14T12:32:33.211Z","updated_at":"2025-12-14T12:32:35.548Z","avatar_url":"https://github.com/adobe.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"######################\n Dynamic SQL (dy-sql)\n######################\n\nThis project consists of a set of python decorators that eases integration with SQL databases.\nThese decorators may trigger queries, inserts, updates, and deletes.\n\nThe decorators are a way to help us map our data in python to SQL queries and vice versa.\nWhen we select, insert, update, or delete the queries, we pass the data we want\nto insert along with a well defined query.\n\nThis is designed to be done with minimal setup and coding. You need to specify\nthe database connection parameters and annotate any SQL queries/updates you have with the\ndecorator that fits your needs.\n\nInstallation\n============\n\n.. code-block::\n\n    pip install dy-sql\n\nDevelopment Setup\n=================\n\nTo set up the development environment:\n\n.. code-block::\n\n    uv sync\n\nComponent Breakdown\n===================\n* **set_default_connection_parameters** - this function needs to be used to set the database parameters on\n  initialization so that when a decorator function is called, it can setup a connection pool to a correct database\n* **is_set_current_database_supported** - this function may be used to determine if the ``*_current_database`` methods\n  may be used or not\n* **set_current_database** - this function may be used to set the database name for the current async context\n  (not thread), this is especially useful for multitenant applications\n* **reset_current_database** - helper method to reset the current database after ``set_current_database`` has\n  been used in an async context\n* **set_database_init_hook** - sets a method to call whenever a new database is initialized\n* **QueryData** - a class that may be returned or yielded from ``sql*`` decorated methods which\n  contains query information\n* **DbMapResult** - base class that can be used when selecting data that helps to map the results of a\n  query to an object in python\n* **DbMapResultModel** - pydantic version of ``DbMapResult`` that allows easy mapping to pydantic models\n* **@sqlquery** - decorator for select queries that can return a SQL result in a ``DbMapResult``\n* **@sqlupdate** - decorator for any queries that can change data in the database, this can take a set of\n  values and yield multiple operations back for insertions or updates inside of a transaction\n* **@sqlexists** - decorator for a simplified select query that will return true if a record exists and false otherwise\n* **XDbTestManager** - test manager classes that may be used for testing purposes\n\nDatabase Preparation\n====================\nIn order to initialize a connection pool for the ``sql*`` decorators, the database needs to first be set up\nusing the ``set_default_connection_parameters`` method.\n\n.. code-block:: python\n\n    from dysql import set_database_parameters\n\n    def set_database_from_config():\n        maria_db_config = {...}\n        set_database_parameters(\n            maria_db_host,\n            maria_db_user,\n            maria_db_password,\n            maria_db_database,\n            port=maria_db_port,\n            charset=maria_db_charset\n        )\n\nNote: the keyword arguments are not required and have standard default values,\nthe port for example defaults to 3306\n\nDatabase Init Hook\n==================\nAt times, it is necessary to perform post-initialization tasks on the database engine after it has been created.\nThe ``set_database_init_hook`` method may be used in this case. As an example, to instrument the engine using\n``opentelemetry-instrumentation-sqlalchemy``, the following code may be used:\n\n.. code-block:: python\n\n    from typing import Optional\n    # Used for type-hints only\n    import sqlalchemy\n    from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor\n    from dysql import set_database_init_hook\n\n    def _instrument_engine(database_name: Optional[str], engine: sqlalchemy.engine.Engine) -\u003e None:\n        # The database name is unused in this case\n        _ = database_name\n        SQLAlchemyInstrumentor().instrument(engine=engine)\n\n    set_database_init_hook(_instrument_engine)\n\n\nMultitenancy\n============\nIn some applications, it may be useful to set a database other than the default database in order to support\ndatabase-per-tenant configurations. This may be done using various provided methods.\n\n.. code-block:: python\n\n    from dysql import (\n        set_default_connection_parameters,\n        reset_current_database,\n        set_current_database,\n        use_database_tenant,\n        tenant_database_manager,\n        sqlquery,\n        QueryData,\n    )\n\n    def init():\n        # Initialize all databases up-front using an arbitrary database key to refer to them later\n        set_default_connection_parameters(\n            ...\n            database_key='db1',\n        )\n        set_default_connection_parameters(\n            ...\n            database_key='db2',\n        )\n\n    def tenant_query_with_manual_set_reset():\n        set_current_database('db2')\n        try:\n            query_database()\n        finally:\n            reset_current_database()\n\n    def tenant_query_with_context_manager():\n        with tenant_database_manager(\"db2\"):\n            return query_database()\n\n    @use_database_tenant(\"db2\")\n    @sqlquery()\n    def tenant_query_with_decorator():\n        return QueryData(\"SELECT * FROM users\")\n\nDecorators\n==========\nDecorators are an easy way for us to tell a function to be a 'query' and return\na result without having to have a big chunk of boiler plate code. Once the\ndatabase has been prepared, calling a ``sql*`` decorated function will initialize\nthe database, parse the value returned in your function, make a corresponding\nparameterized query and return the results.\n\nThe basic structure is to decorate a method that returns information about the query.\nThere are multiple options for returning a query, below is a summary of some of the possibilities:\n\n* return a ``QueryData`` object that possibly contains ``query_params`` and/or ``template_params``\n* (not available for all ``sql*`` decorators) yield one or more ``QueryData`` objects,\n  each containing ``query_params`` and/or ``template_params``\n\nDbMapResult\n~~~~~~~~~~~\nThis class is used in the default mapper (see below) for any ``sqlquery`` decorated method. This class may also be\noverridden as shown below. The default class wraps and returns the results of a query for easy access to the data\nfrom the query. For example, if you use the query ``SELECT id, name FROM table``, it would return a list of\n``DbMapResult`` objects where each contains the ``id`` and ``name`` fields. You could then easily loop through\nand access the properties as shown in the following example:\n\n.. code-block:: python\n\n    @sqlquery()\n    def get_items_from_sql_query():\n        return QueryData(\"SELECT id, name FROM table\")\n\n    def get_and_process_items():\n        for item in get_items_from_sql_query():\n            # we are able to access properties on the object\n            print('{name} goes with {id}'.format(item.name, item.id))\n\nWe can inherit from ``DbMapResult`` and override the way our data maps into the\nobject. This is primarily helpful in cases where we end up with multiple rows\nsuch as a query for a 1-to-many relationship.\n\n.. code-block:: python\n\n    class ExampleMap(DbMapResult):\n        def map_result(self, result):\n            # we know we are mapping multiple rows to a single result\n            if self.id is None:\n                # in our case we know the id is the same so we only set it the first time\n                self.id = result['id']\n                # initialize our array\n                self.item_names = []\n\n        # we know that every result for a given id has a unique item_name\n        self.item_names.append(result['item_name'])\n\n    @sqlquery(mapping=ExampleMap)\n    def get_table_items()\n        return QueryData(\"\"\"\n            SELECT id, name, item_name FROM table\n                JOIN table_item ON table.id = table_item.table_id\n                JOIN item ON item.id = table_item.item_id\n        \"\"\")\n\n    def print_item_names()\n        for table_item in get_table_items():\n            for item_name in table_item.item_names:\n                print(f'table name {table_item.name} has item {item_name}')\n\nDbMapResultModel (pydantic)\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIf pydantic models are desired to be used, there is a record mapper available. Note that pydantic must be installed,\nwhich is available as an extra package:\n\n.. code-block::\n\n    pip install dy-sql[pydantic]\n\nThis model attempts to make mapping records easier, but there are shortcomings of it in more complex cases.\nMost fields will \"just work\" as defined by the type annotations.\n\n.. code-block:: python\n\n    from dysql.pydantic_mappers import DbMapResultModel\n\n    class PydanticDbModel(DbMapResultModel):\n        id: int\n        field_str: str\n        field_int: int\n        field_bool: bool\n\nMapping a record onto this class will automatically convert types as defined by the type annotations. No ``map_record``\nmethod needs to be defined since the pydantic model has everything necessary to map database fields.\n\nLists, sets, dicts, csv strings, and json strings (when using the RecordCombiningMapper) require additional configuration on the model class.\n\n.. code-block:: python\n\n    from dysql.pydantic_mappers import DbMapResultModel\n\n    class ComplexDbModel(DbMapResultModel):\n        # if any data has been aggregated or saved into a string as a comma delimited list, this will convert to a list\n        # NOTE this only does simple splitting and is not fully rfc4180 compatible\n        _csv_list_fields: Set[str] = {'list_from_string'}\n        # List fields (type does not matter)\n        _list_fields: Set[str] = {'list1'}\n        # Set fields (type does not matter)\n        _set_fields: Set[str] = {'set1'}\n        # Dictionary key fields as DB field name =\u003e model field name\n        _dict_key_fields: Dict[str, str] = {'key1': 'dict1', 'key2': 'dict2'}\n        # Dictionary value fields as model field name =\u003e DB field name (this is reversed from _dict_key_fields!)\n        _dict_value_mappings: Dict[str, str] = {'dict1': 'val1', 'dict2': 'val2'}\n        # JSON string fields. Type can be any dictionary type but for larger json objects its safe to stay with `dict`\n        _json_fields: Set[str] = {'json1', 'json2'}\n\n        id: int = None\n        list_from_string: List[str]\n        list1: List[str]\n        set1: Set[str] = set()\n        dict1: Dict[str, Any] = {}\n        dict2: Dict[str, int] = {}\n        json1: dict\n        json2: dict\n\n.. note::\n\n    csv strings can be useful in queries where you want to group by an id and then ``group_concat`` some field\n\n    json strings are a handy way to extract json blobs into a python dictionary for ease of use without manually processing\n    each field everytime you need something.\n\nIn this case, the ``_`` prefixed properties tell the model which fields should be treated differently when combining\nmultiple rows into a single object. For an example of how this works with database rows, see the\n``test_pydantic_mappers.py`` file in the source repository.\n\nNote that validation **does** occur the very first time ``map_record`` is called, but not on subsequent runs. Therefore\nif you desire better validation for list, set, or dict fields, this must most likely be done outside of dysql/pydantic.\nAdditionally, lists, sets, and dicts will ignore null values from the database. Therefore you must provide default\nvalues for these fields when used or else validation will fail.\n\nAdded annotations when using DbMapResultModel\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nWhen using the ``DbMapResultModel`` mapper, there are some additional annotations that may be used to help with\nmapping. These annotations are not required, but may be helpful in some cases.\n\n* FromCSVToList - this annotation will convert a comma separated string into a list. This is useful when you have\n  a column containing a csv or a query that uses ``group_concat`` to combine multiple rows into a single row. This\n  annotation may be used on any field that is a list. For example:\n\n  .. code-block:: python\n\n    from dysql.pydantic_mappers import DbMapResultModel, FromCSVToList\n\n    class CsvModel(DbMapResultModel):\n        id: int\n        name: str\n        # This annotation will convert the string into a list of ints\n        list_from_string_int: FromCSVToList[List[int]]\n        # This annotation will convert the string into a list of strings\n        list_from_string: FromCSVToList[List[str]]\n        # This annotation will convert the string into a list of ints or None if the string is null or empty\n        list_from_string_int_nullable: FromCSVToList[List[int] | None]\n        # This annotation will convert the string into a list of strings or None if the string is null or empty\n        list_from_string_nullable: FromCSVToList[List[str] | None]\n\n        # if using python \u003c= 3.9, you can use typing.Union instead of the pipe operator\n        # list_from_string_nullable: FromCSVToList[Union[List[str],None]]\n\n\n@sqlquery\n~~~~~~~~~\nThis is for making SQL ``select`` calls. An optional mapper may be specified to\nchange the behavior of what is returned from a decorated method. The default\nmapper can combine multiple records into a single result if there is an\n``id`` field present in each record. Mappers available:\n\n* ``RecordCombiningMapper`` (default) - Returns a list of results where multiple records that can be combined with the\n  same unique identifer. An optional ``record_mapper`` value may be passed to the constructor to change\n  how records are mapped to result. By default the ``record_mapper`` used is ``DbMapResult``. The base identifier\n  is the column ``id`` but an array of columns can be used to create a unique key lookup for combining records.\n\n.. note::\n    The ``_key_columns`` field of the ``DbMapResultModel`` is an array containing only the ``id`` but can\n    be overriden in derived classes. For example, setting  ``_key_columns = [ 'a', 'b' ]`` in your derived class\n    would make it so you class would use the values of columns `a` and `b` in order to uniquely identify\n    records when being combined.\n\n* ``SingleRowMapper`` - returns an object for the first record from the database (even if multiple records are\n  returned). An optional ``record_mapper`` value may be passed to the constructor to change how this first record is\n  mapped to the result.\n* ``SingleColumnMapper`` - Returns a list of scalars with the first column from every record, even if multiple columns\n  are returned from the database.\n* ``SingleRowAndColumnMapper`` - Returns a single scalar value even if multiple records and columns are returned\n  from the database.\n* ``CountMapper`` - alias for ``SingleRowAndColumnMapper`` to make it clear that it may be used for ``count`` queries.\n* ``KeyValueMapper`` - returns a dictionary mapping 1 column to the keys and 1 column to the values.\n  By default the key is mapped to the first column and value is mapped to the second column. You can override the key_column\n  and value_columns by specifying the name of the columns you want for each. You can also pass in a has_multiple_values\n  which defaults to False. Doing so will allow you to get a dictionary of lists based on the keys and values you specify.\n* Custom mappers may be made by extending the ``BaseMapper`` class and implementing the ``map_records`` method.\n\nbasic query with conditions hardcoded into query and default mapper\n\n.. code-block:: python\n\n    def get_items():\n        items = select_items_for_joe()\n        # ... work on items\n\n    @sqlquery()\n    def select_items_for_joe()\n        return QueryData(\"SELECT * FROM table WHERE name='joe'\")\n\nbasic query with params passed as a dict\n\n.. code-block:: python\n\n    def get_items():\n        items = select_items_for_name('joe')\n        # ... work on items, which contains all records matching the name\n\n    @sqlquery()\n    def select_items_for_name(name)\n        return QueryData(\"SELECT * FROM table WHERE name=:name\", query_params={'name': name})\n\nquery that only returns a single result from the first row\n\n.. code-block:: python\n\n    def get_joe_id():\n        result = get_item_for_name('joe')\n        return result.get('id')\n\n    # Either an instance or class may be used as the mapper parameter\n    @sqlquery(mapper=SingleRowMapper())\n    def get_item_for_name(name)\n        return QueryData(\"SELECT id, name FROM table WHERE name=:name\", query_params={'name': name})\n\nalternative to the above query that returns the id directly\n\n.. code-block:: python\n\n    def get_joe_id():\n        return get_id_for_name('joe')\n\n    @sqlquery(mapper=SingleRowAndColumnMapper)\n    def get_id_for_name(name)\n        return QueryData(\"SELECT id FROM table WHERE name=:name\", query_params={'name': name})\n\nquery that returns a list of scalar values containing the list of distinct names available\n\n.. code-block:: python\n\n    def get_unique_names():\n        return get_names_from_items()\n\n    @sqlquery(mapper=SingleColumnMapper)\n    def get_names_from_items()\n        return QueryData(\"SELECT DISTINCT(name) FROM table\")\n\nbasic count query that only returns the scalar value returned for the count\n\n.. code-block:: python\n\n    def get_count_for_joe():\n        return get_count_for_name('joe')\n\n    @sqlquery(mapper=CountMapper)\n    def get_count_for_name(name):\n        return QueryData(\"SELECT COUNT(*) FROM table WHERE name=:name\", query_params={'name': name})\n\n\nbasic query returning dictionary\n\n.. code-block:: python\n\n    @sqlquery(mapper=KeyValueMapper())\n    def get_status_by_name():\n        return QueryData(\"SELECT name, status FROM table\")\n\nquery returning a dictionary where we are specifying the keys. Note that the columns are returning in a different order\n\n.. code-block:: python\n\n    @sqlquery(mapper=KeyValueMapper(key_column='name', value_column='status'))\n    def get_status_by_name():\n        return QueryData(\"SELECT status, name FROM table\")\n\nquery returning a dictionary where there are multiple results under each key. Note that here we are essentially grouping under status\n\n.. code-block:: python\n\n    @sqlquery(mapper=KeyValueMapper(key_column='status', value_column='name', has_multiple_values=True))\n    def get_status_by_name():\n        return QueryData(\"SELECT status, name FROM table\")\n\n==========\n@sqlupdate\n==========\nHandles any SQL that is not a select. This is primarily, but not limited to, ``insert``, ``update``, and ``delete``.\n\n\n.. code-block:: python\n\n    @sqlupdate()\n    def insert_items(item_dict):\n        return QueryData(\"INSERT INTO\", template_params={'in__item_id':item_id_list})\n\n\n---------------------------------\nmultiple queries in a transaction\n---------------------------------\nYou can yield multiple QueryData objects. This is done in a transaction and it can be helpful for data integrity or just\na nice clean way to run a set of updates.\n\n.. code-block:: python\n\n    @sqlupdate()\n    def insert_items(item_dict):\n        insert_values_1, insert_params_1 = TemplateGenerator.values('table1values', _get_values_for_1_from_items(item_dict))\n        insert_values_2, insert_params_2 = TemplateGenerator.values('table2values', _get_values_for_2_from_items(item_dict))\n        yield QueryData(f'INSERT INTO table_1 {insert_values_1}', query_params=insert_values_params_1)\n        yield QueryData(f'INSERT INTO table_2 {insert_values_2}', query_params=insert_values_params_2)\n\n--------------------------\ngetting the last insert id\n--------------------------\nYou can assign a callback to be ran after a query or set of queries completes successfully. This is useful when you need\nto get the last insert id for a table that has an auto incrementing id field. This allows you to set it as a parameter on\na follow up relational table within the same transaction scope.\n\n.. code-block:: python\n\n    @sqlupdate()\n    def insert_items_with_callback(item_dict):\n        insert_values_1, insert_params_1 = TemplateGenerator.values('table1values', _get_values_for_1_from_items(item_dict))\n        insert_values_2, insert_params_2 = TemplateGenerator.values('table2values', _get_values_for_2_from_items(item_dict))\n        yield QueryData(f'INSERT INTO table_1 {insert_values_1}', query_params=insert_values_params_1)\n        yield QueryData(f'INSERT INTO table_2 {insert_values_2}', query_params=insert_values_params_2)\n\n    def _handle_insert_success(item_dict):\n        #  callback logic here happens after the transaction is complete\n\n`get_last_insert_id` is a placeholder kwarg that will be automatically overwritten by the sqlupdate decorator at run time.\nTherefore, the assigned value in the function definition does not matter.\n\n\nUsing `get_last_insert_id` gives you the most recently set id. You can leverage this for later queries yielded, or you could\nuse it and set ids in a reference object passed in for access to the ides outside of the sqlupdate function.\n\n\n.. code-block:: python\n\n    @sqlupdate()\n    def insert_item_with_get_last_insert(get_last_insert_id=None, item_dict):\n        insert_values, insert_params = TemplateGenerator.values('table1values', _get_values_from_items(item_dict))\n        yield QueryData(f'INSERT INTO table_1 {insert_values}', query_params=insert_values_params)\n        last_id = get_last_insert_id()\n        yield QueryData(f'INSERT INTO related_table_1 (table_1_id, value) VALUES (:table_1_id, :value)',\n                query_params={'table_1_id': last_id, 'value': 'some_value'})\n\n.. note::\n    `get_last_insert_id` will get you the last inserted id from the most recently table inserted with an autoincrement.\n    Be sure to call `get_last_insert_id` right after you yield the query that inserts the record you need the id for.\n\n\n.. code-block:: python\n\n    class Item(BaseModel):\n        id: int | None = None\n        name: str\n\n    @sqlupdate()\n    def insert_items_and_update_ids(items: List[Item], get_last_insert_id = None)\n        for item in items:\n            yield QueryData(\"INSERT INTO table (name) VALUES (:name)\", query_params={'name': item.name})\n            last_id = get_last_insert_id()\n            item.id = last_id\n\n@sqlexists\n~~~~~~~~~~\nThis wraps a SQL query to determine if a row exists or not. If at least one row is returned from the query, it will\nreturn True, otherwise False. The query you give here can return anything you want but as good practice,\ntry to always select as little as possible. For example, below we are just returning 1 because the value itself\nisn't used, we just need to know there are records available.\n\n.. code-block:: python\n\n    @sqlexists()\n    def item_exists(item_id)\n        return QueryData(\"SELECT 1 FROM table WHERE id=:id\", query_params={'id': item_id})\n\nUltimately, the above query becomes ``SELECT EXISTS (SELECT 1 FROM table WHERE id=:id)``.\nYou'll notice the inner select value isn't actually used in the return.\n\nDecorator templates\n===================\n\nTemplates and generators for these templates are also provided to simplify SQL query strings.\n\n\n**in** template - this template will allow you to pass a list as a single parameter and have the `IN`\ncondition build out for you. This allows you to more dynamically include values in your queries.\n\n.. code-block:: python\n\n    @sqlquery()\n    def select_items(item_id_list):\n        return QueryData(\"SELECT * FROM table WHERE {in__item_id}\",\n                        template_params={'in__item_id': item_id_list})\n\n\nyou can also use the TemlpateGenerate.in_column method to get back a tuple of query and params\n\n.. code-block:: python\n\n    @sqlquery()\n    def select_items(item_id_list):\n        in_query, in_params = TemplateGenerators.in_column('key', item_id_list)\n        # NOTE: the query string is using an f-string and passing into query_params instead of template_params\n        return QueryData(f\"SELECT * FROM table WHERE {in_query}\", query_params=in_params)\n\n\n**in and not in multi column** - this template works the same as the in and not in template but it will allow you to\npass a list of tuples to an in clause allowing you to match against multiple columns.\n`NOTE: this is only available through the TemplateGenerators using query_params and not through the the template_params method`\n\n.. code-block:: python\n\n    @sqlquery()\n    def select_multi(tuple_list):\n        in_query, in_params = TemplateGenerators.in_multi_column('(key1, key2)', tuple_list)\n        return QueryData(f\"SELECT * FROM table WHERE {in_query}\", query_params=in_params)\n\n\n.. code-block:: python\n\n    @sqlquery()\n    def select_multi(tuple_list):\n        in_query, in_params = TemplateGenerators.not_in_multi_column('(key1, key2)', tuple_list)\n        return QueryData(f\"SELECT * FROM table WHERE {in_query}\", query_params=in_params)\n\n\n**not_in** template -  this template will allow you to pass a list as a single parameter and have the `NOT IN`\ncondition build out for you. This allows you more dynamically exclude values in your queries.\n\n.. code-block:: python\n\n    @sqlquery()\n    def select_items(item_id_list)\n        return QueryData(\"SELECT * FROM table WHERE {not_in__item_id}\",\n                        template_params={'not_in__item_id': item_id_list})\n\n\n\n\nyou can also use the TemplateGenerators.not_in_column method to get back a tuple of query and params\n\n.. code-block:: python\n\n    @sqlquery()\n    def select_items(item_id_list):\n        not_in_query, not_in_params = TemplateGenerators.not_in_column('key', item_id_list)\n        # NOTE: the query string is using an f-string and passing into query_params instead of template_params\n        return QueryData(f\"SELECT * FROM table WHERE {not_in_query}\", query_params=not_in_params)\n\n\n**values** template - when inserting and you have multiple records to insert, this allows you to pass\nmultiple records for insert in a single INSERT statement.\n\n.. code-block:: python\n\n    @sqlquery()\n    def insert_items(items):\n        return QueryData(\"INSERT_INTO table(column_a, column_b) {values__items}\",\n                        template_params={'values__items': item_id_list})\n\nYou can write queries that combine ``template_params`` and ``query_params`` as well..\n\n.. code-block:: python\n\n    @sqlquery()\n    def select_items(item_id_list, name):\n        return QueryData(\"SELECT * FROM table WHERE {in__item_id} and name=:name\",\n                        template_params={'in__item_id': item_id_list},\n                        query_params={'name': name})\n\nTesting with Managers\n=====================\n\nDuring testing, it may be useful to hook up a real database to the tests. However, this can be difficult to maintain\nschema and isolate databases during testing. Database test managers exist for this reason. Usage is very simple with\npytest.\n\n.. code-block:: python\n\n    @pytest.fixture(scope='module', autouse=True)\n    def setup_db(self):\n        # Pass in the database name and any optional params\n        with MariaDbTestManager(f'testdb_{self.__class__.__name__.lower()}'):\n            yield\n\nThe Maria database test manager is shown used above, but future implementations may be added for other SQL backends.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadobe%2Fdy-sql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadobe%2Fdy-sql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadobe%2Fdy-sql/lists"}