Package: twinsqla
TWinSQLATWinSQLA is a light framework for mapping SQL statements to python functions and methods.FeaturesAvailable in Python 3.6+We recommends Python 3.7+ since available to use@dataclasses.dataclassdecorator in entity classes.This framework concept is avoid ORM features!
Coding with almost-raw SQL query (with prepared parameters) simply.If you can use SQL query with coding simply, it make you to skipping the times of converting python coding
with ORM features and checking result.TWinSQLA support you to checking only SQL query without coding with ORM features.Support "two-way SQL" template."Two-way SQL" templates can be executed SQL statements with dynamic parameter written by python expression.In "two-way SQL", dynamic parameters and conditional expressions are surrounded by '/*' and '*/'.
So, "two-way SQL" templates are available to execute in SQL tools as they are.TWinSQLa is inspired byDoma,
which is Java framework for accessing databases.SinceSQLAlchemycore is used for accessing databases,
SQLAlchemy core features can be utilized. (such as connection pool)How to installYou can install from PyPI by the follow command.pipinstalltwinsqlaUsageFirst step (In case that TWinSQLA object is available in global scope)fromtypingimportOptionalfromcollectionsimportOrderedDictimportsqlalchemyfromtwinsqlaimportTWinSQLAengine:sqlalchemy.engine.base.Engine=sqlalchemy.create_engine(...)sqla:TWinSQLA=TWinSQLA(engine)classStaffDao():@sqla.select("SELECT * FROM staff WHERE staff_id = /* :staff_id */1")deffind_by_id(self,staff_id:int)->Tuple[OrderDict,...]:passAt first, create instance of sqlalchemy engine, and create TWinSQLA instance with sqlalchemy engine.engine:sqlalchemy.engine.base.Engine=sqlalchemy.create_engine(...)sqla:TWinSQLA=TWinSQLA(engine)To execute select query, usesqla.selectdecorator. In this case,sqlais TWinSQLA instance.@sqla.select("SELECT * FROM staff WHERE staff_id = /* :staff_id */1")deffind_by_id(self,staff_id:int)->Tuple[OrderDict,...]:passThe above example, select query in decorator's argument is written as "two-way SQL."
When calleddao.find_by_id(staff_id=10), then the like following code will be executed.>query=sqlalchemy.sql.text("SELECT * FROM staff WHERE staff_id = :staff_id")>engine.execute(query,{staff_id:10})The execution results will be converted to sequence of OrderedDict, and returned from the above method.Thesqla.selectdecorator can return object for your custom class, or return the results iterable.  For more details, see the other section.In production usageFor about production usage, you may separate source codes as dao classes, entity classes, and handling transaction classes.fromdataclassesimportdataclass,fieldimporttwinsqlafromtwinsqlaimportTWinSQLA,table,autopk# Entity class@dataclass(frozen=True)@table("staff",pk=autopk("staff_id"))classStaff:staff_id:int=field(default=None)staff_name:str=field(default=None)age:int=field(default=None)# Dao classclassStaffDao:def__init__(self,sqla:TWinSQLA):self.sqla:TWinSQLA=sqla@twinsqla.select("SELECT * FROM staff WHERE staff_id >= /* :more_than_id */2",result_type=List[Staff])deffetch(self,more_than_id:int)->List[Staff]:pass@twinsqla.insert()definsert(self,staff:Staff):pass# Service class (Handling database transaction)classStaffService:def__init__(self,sqla:TWinSQLA):self.sqla:TWinSQLA=sqlaself.staff_dao:StaffDao=StaffDao(sqla)deffind_staff(self,more_than_id:int)->List[Staff]:returnself.staff_dao.fetch(more_than_id)defregister(self,staff_name:str,age:int):new_staff:Staff=Staff(staff_name=staff_name,age=age)# DB transaction scopewithself.sqla.transaction():self.staff_dao.insert(new_staff)Dao classInitializingIn this cases, the TWinSQLA object is not existed in global scope but only in dao instance scope. So, you cannot use TWinSQLA instance decorators (for example :@sqla.select()) at the dao methods.
Instead of using instance decorators, you can use TWinSQLA function decorators. (for example :@twinsqla.select())When executing, the TWinSQLA function decorators search TWinSQLA object. By this search, TWinSQLA instance can be found specified by one of the follow ways.By configured with instance parameter with named 'sqla'. (above the sample code)def__init__(self,sqla:TWinSQLA):self.sqla:TWinSQLA=sqlaOr, the other way, by specified with method arguments with named 'sqla'.@twinsqla.select(...)deffetch(self,sqla:TWinSQLA,more_than_id:int)->List[Staff]:passSelectTo executing select query, you need to usetwinsqla.select()function decorator instead ofsqla.select()instance decorator.@twinsqla.select("SELECT * FROM staff WHERE staff_id >= /* :more_than_id */2",...)deffetch(self,more_than_id:int)->...:passYou can customise the results of select query by the decorator's argumentresult_type.
The argumentresult_typeneeds to be specified a class or sequence of a class.
In the case that results of select query is more than one object, you need specify theresult_typeas sequence of a class. (for example,List[...])@twinsqla.select(...,result_type=List[Staff])deffetch(...)->List[Staff]:passIn the above code, each one result of select query is convert to Staff instance, andfetch()method returns list of Staff.InsertOther examples, to insert a record, you can usetwinsqla.insert()function decorator.@twinsqla.insert()definsert(self,staff:Staff):passTheinsert()decorator automatically build insert query withStaffinstance which class decorated by@table()with table_name.>query=sqlalchemy.sql.text("INSERT INTO staff(staff_name, age) VALUES (:staff_name, :age)")>engine.execute(query,{staff_name:staff.staff_name,age:staff.age})By other way, you can build insert query by your hand as following.@twinsqla.insert("INSERT INTO staff(staff_name, age) VALUES (:staff_name, :age)")definsert(self,staff_name:str,age:int):passEntity classResult of selectEntity class of select query needs to have the constructor with arguments of listed column namesclassStaff:def__init__(self,staff_id:int,staff_name:str,age:int):self.staff_id:int=staff_idself.staff_name:str=staff_nameself.age:int=ageThe above code can be replaced to the following with decorated by@dataclass().fromdataclassesimportdataclass@dataclass(frozen=True)classStaff:staff_id:intstaff_name:strage:intInsertEntity class of insert query with automatically building needs to be decorated by@table()with argument of the table name and have attributes for inserting.@dataclass(frozen=True)@table("staff",pk=autopk("staff_id"))classStaff:staff_id:int=field(default=None)staff_name:str=field(default=None)age:int=field(default=None)In the above code, use theStaffinstance can insert into 'staff' table with columns 'staff_name' and 'age'. The column 'staff_id' is removed in insert query by specifiedautopk('staff_id')in@table()'spkargument.autopk('column_name')means that the value of primary columncolumn_nameis auto-increment by database.This entity class can be also available in return type of select query and argument of update / delete queries.Auto generating entity codesTWinSQLA supports that automatically generating entity codes for existing database.Example$twinsqlacodegen--to_file{path/to/output}{databaseurl}For more details, runtwinsqlacodegen -hin your terminal.TransactionIn using TWinSQLA,TWinSQLA.transaction()can handle database transaction by context manager via sqlalchemy api.withsqla.transaction():# execute queryWhen any exceptions are not occured in context block, then database transaction are commited. Otherwise, if any exceptions are occured, database transaction will be rollbacked and sqlalchemy exception are raised over context bock.ExceptionsIn using TWinSQLA, two type base exceptions may be occured.twinsqla.exceptions.TWinSQLAExceptionsqlalchemy.exc.SQLAlchemyErrorTWinSQLAExceptionis occured when your queries or implementation are invalid.
The other hand,SQLAlchemyErroris raised by sqlalchemy.In implementation, you need to consider about handlingsqlalchemy.exc.DBAPIError, which raised in database operation failed.API Referencetwinsqla.TWinSQLAdef__init__(self,engine:sqlalchemy.engine.base.Engine,*,available_dynamic_query:bool=True,sql_file_root:Optional[Union[Path,str]]=None,cache_size:Optional[int]=128):..."""Args:engine (sqlalchemy.engine.base.Engine): SQLAlchemy engine instance.available_dynamic_query (bool, optional):If True, then two-ways SQL is available.If False, sql statements are not converted in executingbut executed as it is specified. Defaults to True.sql_file_root (Optional[Union[Path, str]], optional):Specify the root directory of sql files. Defaults to None.cache_size (Optional[int], optional):Cache size of loaded query function. Defaults to 128."""TWinSQLA.transaction()twinsqla.select(),TWinSQLA.select()defselect(query:Optional[str]=None,*,sql_path:Optional[str]=None,result_type:Type[Any]=Tuple[OrderedDict,...],iteratable:bool=False):"""Function decorator of select operation.Only one argument `query` or `sql_path` must be specified.In called decorated method, the processing implemented by the methodis not executed, but arguments of method are used for bind parameters.Args:query (Optional[str], optional):select query (available TwoWay SQL). Defaults to None.sql_path (Optional[str], optional):file path with sql (available TwoWay SQL). Defaults to None.result_type (Type[Any], optional):return type. Defaults to Tuple[OrderedDict, ...].iteratable (bool, optional):When you want to fetching iterataly result, then True specifiedand returned ResultIterator object. Defaults to False.Returns:Callable: Function decorator"""twinsqla.insert(),TWinSQLA.insert()definsert(query:Optional[str]=None,*,sql_path:Optional[str]=None,table_name:Optional[str]=None,result_type:Type[Any]=None,iteratable:bool=False):"""Function decorator of insert operation.In constructing insert query by yourself, you need to specify eitherone of the arguments `query` or `sql_path`.In neither `query` nor `sql_path` are specified, this decorator createsinsert query with arguments of decorated method.In this case, you need to specify inserted table name by decoratorargument 'table_name' or decorating '@twinsqla.table' to entity class.Args:query (Optional[str], optional):insert query (available TwoWay SQL). Defaults to None.sql_path (Optional[str], optional):file path with sql (available TwoWay SQL). Defaults to None.table_name (Optional[str], optional):table name for inserting. Defaults to None.result_type (Type[Any], optional):When constructing "INSERT RETURNING" query, it is useful tospecify return type. Defaults to None.iteratable (bool, optional):In almost cases, this argument need not to specified.The only useful case is in using "INSERT RETURNING" query.Defaults to False.Returns:Callable: Function decorator for insert query"""twinsqla.update(),TWinSQLA.update()defupdate(query:Optional[str]=None,*,sql_path:Optional[str]=None,table_name:Optional[str]=None,condition_columns:Optional[Union[str,Tuple[str,...]]]=None,result_type:Type[Any]=None,iteratable:bool=False):"""Function decorator of update operation.In constructing update query by yourself, you need to specify eitherone of the arguments `query` or `sql_path`.In neither `query` nor `sql_path` are specified, this decorator createsupdate query with arguments of decorated method.In this case, you need follows.1. To specify updated table nameby decorating '@twinsqla.table' to entity class.or by decorator argument 'table_name'2. To specifry the column names for using WHERE conditionsby decorating '@twinsqla.table' with `pk` parameteror by decorator argument 'condition_columns'.Args:query (Optional[str], optional):update query (available TwoWay SQL). Defaults to None.sql_path (Optional[str], optional):file path with sql (available TwoWay SQL). Defaults to None.table_name (Optional[str], optional):table name for updating. Defaults to None.condition_columns (Optional[Union[str, Tuple[str, ...]]], optional):column names in WHERE condition. In almost cases, you arerecommended to specify primary key names of the table.Defaults to None.result_type (Type[Any], optional):When constructing "UPDATE RETURNING" query, it is useful tospecify return type. Defaults to None.iteratable (bool, optional):In almost cases, this argument need not to specified.The only useful case is in using "UPDATE RETURNING" query.Defaults to False.Returns:Callable: Function decorator for update query"""twinsqla.delete(),TWinSQLA.delete()defdelete(query:Optional[str]=None,*,sql_path:Optional[str]=None,table_name:Optional[str]=None,condition_columns:Optional[Union[str,Tuple[str,...]]]=None,result_type:Type[Any]=None,iteratable:bool=False):"""Function decorator of delete operation.In constructing delete query by yourself, you need to specify eitherone of the arguments `query` or `sql_path`.In neither `query` nor `sql_path` are specified, this decorator createsdelete query with arguments of decorated method.In this case, you need follows.1. To specify updated table nameby decorating '@twinsqla.table' to entity class.or by decorator argument 'table_name'2. To specifry the column names for using WHERE conditionsby decorating '@twinsqla.table' with `pk` parameteror by decorator argument 'condition_columns'.Args:query (Optional[str], optional):delete query (available TwoWay SQL). Defaults to None.sql_path (Optional[str], optional):file path with sql (available TwoWay SQL). Defaults to None.table_name (Optional[str], optional):table name for deleting. Defaults to None.condition_columns (Optional[Union[str, Tuple[str, ...]]], optional):column names in WHERE condition. In almost cases, you arerecommended to specify primary key names of the table.Defaults to None.result_type (Type[Any], optional):When constructing "DELETE RETURNING" query, it is useful tospecify return type. Defaults to None.iteratable (bool, optional):In almost cases, this argument need not to specified.The only useful case is in using "DELETE RETURNING" query.Defaults to False.Returns:Callable: Function decorator for delete query"""twinsqla.execute(),TWinSQLA.execute()defexecute(query:Optional[str]=None,*,sql_path:Optional[str]=None,result_type:Type[Any]=Tuple[OrderedDict,...],iteratable:bool=False):"""Function decorator of any operation.Only one argument `query` or `sql_path` must be specified.In called decorated method, the processing implemented by the methodis not executed, but arguments of method are used for bind parameters.Args:query (Optional[str], optional):any query (available TwoWay SQL). Defaults to None.sql_path (Optional[str], optional):file path with sql (available TwoWay SQL). Defaults to None.result_type (Type[Any], optional):return type. Defaults to Tuple[OrderedDict, ...].iteratable (bool, optional):When you want to fetching iterataly result, then True specifiedand returned ResultIterator object. Defaults to False.Returns:Callable: Function decorator"""SQL TemplateBind variableTWinSQLA's two-way SQL can handle the bind parameter namedsome_parameteras follow./* :some_parameter */_dummy_value_Where,dummy_valueis ignored in TWinSQLA dynamic query.Implementation.@twinsqla.select("SELECT * FROM table_name WHERE key = /* :some_value */300")deffetch_by_key(self,some_value:int)->dict:passCalling methods.dao.fetch_by_key(10)In this case, the follow statement and codes are executed.query=sqlalchemy.sql.text("SELECT * FROM table_name WHERE key = :some_value")sqlalchemy_engin.execute(query,{"some_value":10})Bind variable with iterator(Not yet implemented)(Not yet implemented handling iterable binding parameter)SELECT*FROMtable_nameWHEREkeysIN/* :some_values */(300,305,317)Python expression variableTWinSQLA's two-way SQL can embed a python expressions in sql statements as follow./* python_expression */_dummy_value_Where,dummy_valueis ignored in TWinSQLA query.Implementation.@twinsqla.select("SELECT * FROM table_name WHERE key = /* some_value * 100 */300")deffetch_by_key(self,some_value:int)->dict:passIn this case,some_value * 100is the python expression, andsome_valuemust be specified in this method's arguments.Call methods.dao.fetch_by_key(10)Then the follow statement and codes are executed.query=sqlalchemy.sql.text("SELECT * FROM table_name WHERE key = :dynamic_param")sqlalchemy_engin.execute(query,{"dynamic_param":10*100})This bind parameter:dynamic_paramis automatically generated by TWinSQLA to assign the python expressionsome_value * 100to this bind parameter.IF block (Basic usage)Definition of dynamic if-block/*%if _python_expression_ */sql_expression[/*%elif _python_expression_ */dummy_opsql_expression[...]][/*%else*/dummy_opsql_expression]/*%end*/dummy_op:="AND"|"OR"Implementation@twinsqla.select(r"""SELECT * FROM table_nameWHERE/*%if some_value == 'first' */some_column1 > 0/*%elif some_value == 'second' */OR some_column2 > 0/*%else*/OR some_column1 = 0 AND some_column2 = 0/*%end*/""")deffind(self,some_value:str)->List[dict]:passCall sample1.dao.find("first")Then query1 is:SELECT*FROMtable_nameWHEREsome_column1>0By the first if-condition is satisfied, then the others expressions are ignored.Call sample2.dao.find("second")Then query2 is:SELECT*FROMtable_nameWHEREsome_column2>0By the first if-condition is not satisfied and second is, then the excepts for second expression are ignored. And, noticed thatORoperation ahead of expressionsome_column2 > 0is ignored.Call sample3.dao.find("other")Then query3 is:SELECT*FROMtable_nameWHEREsome_column1=0ANDsome_column2=0IF block (Advanced usage)Nested IF blockIF block can be nested.Example.@twinsqla.select(r"""SELECT * FROM table_nameWHERE/*%if some_value1 == 'first' */some_column1 > 0/*%elif some_value1 == 'second' */OR some_column2 > 0 AND/*%if some_value2 > 0 */some_column3 = some_column4/*%else*/OR some_column3 IS NULL/*%end*//*%else*/OR some_column1 = 0 AND some_column2 = 0/*%end*/""")deffind(self,some_value1:str,some_value2:int)->List[dict]:passAbout python expression nested in if-blocks evaluationPython expression variables nested in if-blocks are evaluated only when if-condition is satisfied.
Consider the follow example with if-block and python expression variable.@twinsqla.select(r"""SELECT * FROM table_nameWHERE/*%if some_value1 != 0 */some_column1 > /* some_value2 / some_value1 */10/*%else*/OR some_column1 > 0/*%end*/""")deffind(self,some_value1:int,some_value2:int)->List[dict]:passIn the first case, the follow calling has no problem.dao.find(10,50)The above calling is the almost same the following execution.query=sqlalchemy.sql.text("""SELECT * FROM table_nameWHEREsome_column1 > :dynamic_param""")sqlalchemy_engin.execute(query,{"dynamic_param":(50/10)})In the next case, the follow calling.dao.find(0,7)In this case.query=sqlalchemy.sql.text("""SELECT * FROM table_nameWHEREsome_column1 > 0""")sqlalchemy_engin.execute(query,{"dynamic_param":None})Because for the first if-conditionsome_value1 != 0is not satisfied, the first python expression variable is not evaluated. (In detail, evaluated asNonewithout evaluating dividing by zero5 / 0.)FOR block(Not yet implemented)sampleSELECT*FROMtable_nameWHERE/*%for item in iterator */some_column=/* item */'dummy'/*%or*//*%end*/