Package: zugh
Zugh[WIP] Access to database in pythonic wayZughStatusRequiredLicenceInstallUsageConnectionConfigPoolDatabaseTableQuery ObjectInsertInsert a RowInsert IgnoreInsert Or UpdateInsert Multi RowsSelectFilterLogic ExpressCompareAliasSortLimitAggregateDistinctSubqueryJoin InUnionUpdateF ObjectDeleteDecoratordb.query.querydb.query.transactionStringS ObjectMathZughis a tool for generating SQL and accessing databases flexibly in pythonic way. It empower you to use complex SQL, but didn't need to write them directly.StatusWork in progress.Now we support MySQL only.RequiredPython >= 3.6PyMySQL >= 0.9.3LicenceMIT.InstallUse pip:pipinstallzughUsageNote !The time of writing each part of this document is out of order. So the results before and after the execution of SQL may not match. Nevertheless, I recommend that you start reading from scratch and try the code.ConnectionConfig>>>fromzugh.db.connectionimportconnect_config>>>conn_config=connect_config('localhost','your_username','your_password')# You can use conn_config dict to configure connection for DdataBase object# or initial a connection poolPool>>>fromzugh.db.poolimportConnectionPool>>>pool=ConnectionPool(conn_config)DatabaseCreate a database:>>>fromzugh.schema.dbimportDataBase>>>db=DataBase('zugh',conn_config=conn_config)# or db = DataBase('zugh', pool=pool)>>>db.create()TableCreate a table.We haven't implemented those APIs to create a table yet, so just execute SQL with a connection:>>>fromzugh.dbimportconnect>>>sql="""CREATE TABLE zugh.users (`id` int(11) NOT NULL AUTO_INCREMENT,`age` int(11) NOT NULL,`score` int(11) NOT NULL DEFAULT '0',PRIMARY KEY (`id`)) Engine=InnoDB DEFAULT CHARSET=utf8mb4;""">>>conn=connect(conn_config)# return a connection context>>>withconnascn:withcn.cursor()ascursor:cursor.execute(sql)Initial aTableobject:>>>fromzugh.schema.tableimportTable>>>tb=Table('users',db)Query Objectzugh.query.core.QueryBaseprovide a base class for Query class below:zugh.query.core.SelectBasezugh.query.core.Updatezugh.query.core.Insertzugh.query.core.Deleteor subclass of above classQuery objectis a instance of above class. If they were printed, a string of SQL statement would output.If configure properly, they can call.exe()method to execute. Usually, you don't use them
directly.Mostly, you would initial azugh.schema.table.Tableinstance and call relative method, then will return a newQuery object.Dangerous queries, such asupdate,deleteor similar mothed are expose afterTable.where()method.>>>q_1=tb.where().select()>>>type(q_1)<class'zugh.query.core.Select'>>>>q_2=tb.where().update(age=10)>>>type(q_2)<class'zugh.query.core.Update'>>>>q_3=tb.where().delete()>>>type(q_3)<class'zugh.query.core.Delete'>>>>q_4=tb.insert(dict(age=10,score=18))>>>type(q_4)<class'zugh.query.core.Insert'>>>>q_5=q_1.order_by()>>type(q_5)<class'zugh.query.core.OrderBy'>InsertInsert a Row>>>q1=tb.insert(age=16,score=7)>>>print(q1)INSERTINTOzugh.users(age,score)VALUES(16,7)>>>q2=tb.where().select()>>>print(q2)SELECT*FROMzugh.users>>>q2.exe()# execute q2((),0)>>>q1.exe()# execute q11>>>q2.exe()# execute q2 again(((1,16,7),),1)Insert Ignore>>>q3=tb.insert_ignore(id=1,age=16,score=7)>>>print(q3)INSERTIGNOREINTOzugh.users(id,age,score)VALUES(1,16,7)>>>q3.exe()# would show a duplicate key warningInsert Or UpdateYou can useFobject orvaluesobject to complete complex query.fromzugh.query.othersimportF,values>>>row=dict(id=1,age=16,score=7)>>>q4=tb.upsert(row,dict(age=9))>>>print(q4)INSERTINTOzugh.users(id,age,score)VALUES(1,16,7)ONDUPLICATEKEYUPDATEage=9>>>update_fv=dict(age=F('age')-1,score=values('age')+1)>>>q5=tb.upsert(row,update_fv=update_fv)>>>print(q5)INSERTINTOzugh.users(id,age,score)VALUES(1,16,7)ONDUPLICATEKEYUPDATEage=age-1,score=VALUES(age)+1Insert Multi Rows>>>rows=[dict(age=9,score=8),dict(age=7,score=9),dict(age=17,score=7),dict(age=23,score=7),]>>>q6=tb.insert_multi(rows)>>>print(q6)INSERTINTOzugh.users(age,score)VALUES(9,8),(7,9),(17,7),(23,7)>>>q6.exe()# execute q64>>>q2.exe()(((1,16,7),(2,9,8),(3,7,9),(4,17,7),(5,23,7)),5)Select>>>q7=tb.where(id=3).select()>>>print(q7)SELECT*FROMzugh.usersWHEREid=3>>>q7.exe()(((3,7,9),),1)>>>q8=tb.where().select('id','age')>>>print(q8)SELECTid,ageFROMzugh.users>>>q8.exe()(((1,16),(2,9),(3,7),(4,17),(5,23)),5)By default,  when call.exe()of Select Query, it will return atuplewhich contain queryset and number of rows.
The format of queryset format is stilltuple. If you prefer a dict-format queryset, you should pass a parametercursorclass=pymysql.cursors.DictCursorto configure connection. For more infomation, please refer docs ofPyMySQL.FiltertheTable.where()method of Table instance act as a filter.If don't need to filter table, You can callTable.select()directly. It is a shortup ofTable.where().select().Logic Express>>>fromzugh.query.logicimportAND,OR,L>>>q9=tb.where('id>3','id<7').select()>>>print(q9)SELECT*FROMzugh.usersWHEREid>3ANDid<7>>>q9.exe()(((4,17,7),(5,23,7)),2)>>>w1=L('id<3')|L('id>7')# equal to OR('id<3', 'id>7')>>>w2=OR('id<3','id>7')>>>w1OR(L(id<3),L(id>7))>>>w2OR(L(id<3),L(id>7))>>>print(w1)id<3ORid>7>>>print(w2)id<3ORid>7>>>q10=tb.where(w2).select()>>>print(q10)SELECT*FROMzugh.usersWHERE(id<3ORid>7)>>>q10.exe()(((1,16,7),(2,9,8)),2)# you can combine complex Logic object use L, OR and AND>>>w3=L('id>3')&L('id<7')# equal to AND('id>3', 'id<7')>>>print(w3)id>3ANDid<7>>>w4=L('age>3')&L('age<20')>>>print(w4)age>3ANDage<20>>>print(OR(w3,w4))(id>3ANDid<7)OR(age>3ANDage<20)>>>print(w3|w4)(id>3ANDid<7)OR(age>3ANDage<20)CompareWe use class or their instance to deal with compare express.You can find them inzugh.query.conditionmodule.SQL OperatorPython Class/Instance/Operatorexample=eq,=.where(name='lisa')!=ne.where(name=ne('lisa'))>gt.where(amount=gt(9))>=ge.where(amount=ge(9))<lt.where(amount=lt(6))<=le.where(amount=le(5))INIn.where(id=In(1,2,3,4))NOT INNIn.where(id=NIn(98,34,2))LIKElike.where(name=like('lisa%'))NOT LIKEunlike.where(name=unlike('john%'))IS NULLNULL.where(age=NULL)IS NOT NULLNOT_NULL.where(age=NOT_NULL)Though works,eqis meaningless. For convenience, you would always use=.>>>fromzugh.query.conditionimportNOT_NULL,NULL,In,NIn,ge,gt,le,like,lt,ne,unlike>>>q11=tb.where(id=gt(3)).select()>>>print(q11)SELECT*FROMzugh.usersWHEREid>3>>>q12=tb.where(id=gt(3),age=lt(18)).select()>>>print(q12)SELECT*FROMzugh.usersWHEREid>3ANDage<18>>>q12.exe()(((4,17,7),),1)>>>q13=tb.where(id=In(1,3,5,7,9)).select('id','score')>>>print(q13)SELECTid,scoreFROMzugh.usersWHEREidIN(1,3,5,7,9)>>>q13.exe()(((1,7),(3,9),(5,7)),3)>>>q14=tb.where(score=NULL).select()>>>print(q14)SELECT*FROMzugh.usersWHEREscoreISNULLAlias>>>fromzugh.query.othersimportAs>>>fromzugh.query.aggregateimportMax>>>qa=tb.where().select(max_age=Max('age'))>>>print(qa)SELECTmax(age)ASmax_ageFROMzugh.users>>>print(tb.where().select(As(Max('age'),'max_age')))SELECTmax(age)ASmax_ageFROMzugh.usersWe support alias, but the defaultcursorclassof PyMySQL will return query set in tuple. In this case, alias is useless.
If you want to return dict, you need to configure connection parametercursorclass=pymysql.cursors.DictCursor. For more information, please refer PyMySQL's documents.Sort>>>q15=tb.where().select().order_by('age')>>>print(q15)SELECT*FROMzugh.usersORDERBYage>>>q15.exe()(((3,7,9),(2,9,8),(1,16,7),(4,17,7),(5,23,7)),5)# You can use prefix '-' to sort reverse.>>>q16=tb.where().select().order_by('-age','score')>>>print(q16)SELECT*FROMzugh.usersORDERBYageDESC,score>>>q16.exe()(((5,23,7),(4,17,7),(1,16,7),(2,9,8),(3,7,9)),5)LimitWe use a magicalsliceto act limit/offset,Select Query' slice will return a instance ofzugh.query.core.Limit, which is a subclass ofzugh.query.core.SelectBase.>>>qm=tb.where().select()>>>qm1=qm[:3]# fetch frist three>>>print(qm1)SELECT*FROMzugh.usersLIMIT3>>>qm2=qm[2:4]>>>print(qm2)SELECT*FROMzugh.usersLIMIT2,2>>>qm3=qm[2:]>>>print(qm3)SELECT*FROMzugh.usersLIMIT2,18446744073709551614Except for instances ofLimit, all the others instance ofSelectBasecould use slice to return a instance ofLimit.The slice here don't accept negative numbers.AggregateWe provide some aggregation functions inzugh.query.aggregatemoulde.>>>fromzugh.query.aggregateimportAvg,Count,Max,Min,Sum>>>q17=tb.where().select(Avg('age'))>>>print(q17)SELECTavg(age)FROMzugh.users>>>q17.exe()(((Decimal('14.4000'),),),1)>>>q18=tb.where().select('score',Count('id')).group_by('score')>>>print(q18)SELECTscore,count(id)FROMzugh.usersGROUPBYscore>>>q18.exe()(((7,3),(8,1),(9,1)),3)You can also use write 'raw' functions as long as you like it, such as:q17=tb.where().select('avg(age)')Distinctfromzugh.query.othersimportdistinct>>>q19=tb.where().select('distinct age','score')>>>print(q15)SELECTdistinctage,scoreFROMzugh.users>>>q19.exe()(((16,7),(9,8),(7,9),(17,7),(23,7)),5)>>>q20=tb.where().select(distinct('age'),'score')>>>print(q20)SELECTDISTINCTage,scoreFROMzugh.users>>>q21=tb.where().select(Count(distinct('age')))>>>print(q21)SELECTcount(DISTINCTage)FROMzugh.usersSubqueryAt present,InandNInexpress support subquery, it could accept a instance ofSelectBaseas a parameter.But they can not accept instance ofzugh.schema.table.TempTableas a parameter.TempTableaccept aSelect Queryas frist parameter and a alias string as second parameter. It would act like a normal read-only table, you could join it with others, or query data as a new instanceTempTable.>>>fromzugh.schema.tableimportTempTable>>>q22=tb.where().select(Max('age'))>>>print(q22)SELECTmax(age)FROMzugh.users>>>q23=tb.where(age=In(q22)).select()>>>print(q23)SELECT*FROMzugh.usersWHEREageIN(SELECTmax(age)FROMzugh.users)>>>q23.exe()(((5,23,7),),1)>>>q_t=tb.where(id=gt(2)).select()>>>tb_t1=q_t.as_table('ak')# equal to tb_t1 = TempTable(q_t, 'ak')>>>tb_t1TempTable(SELECT*FROMzugh.usersWHEREid>2)>>>>>>sql2="""CREATE TABLE zugh.account (`id` int(11) NOT NULL AUTO_INCREMENT,`user_id` int(11) NOT NULL,`amount` decimal(11,2) NOT NULL DEFAULT '0',PRIMARY KEY (`id`)) Engine=InnoDB DEFAULT CHARSET=utf8mb4;""">>>withconnascn:withcn.cursor()ascursor:cursor.execute(sql2)>>>tb2=Table('account',db,alias='a')# If tend to join table, alias is necessary>>>rows2=(dict(user_id=1,amount='99.89'),dict(user_id=2,amount='292.2'),dict(user_id=3,amount='299.89'),dict(user_id=4,amount='192.1'),dict(user_id=5,amount='183.7'),)>>>tb2.insert_multi(rows2).exe()>>>>>>tb_t2=tb_t1.inner_join(tb2,on='a.user_id = ak.id')>>>q_t2=tb_t2.select()>>>print(q_t2)SELECT*FROM(SELECT*FROMzugh.usersWHEREid>2)ASakINNERJOINzugh.accountASaONa.user_id=ak.id>>>q_t2.exe()(((3,7,8,3,3,Decimal('299.89')),(4,17,8,4,4,Decimal('192.10'))),2)Join InLet's add a new table and query from the join table:>>>tb3=Table('users',db,alias='b')# If tend to join table, alias is necessary>>>tb_i=tb2.inner_join(tb3,on='a.user_id=b.id')>>>q_i=tb_i.where(a__id=gt(2)).select('a.id','a.user_id','a.amount','b.score','b.age')>>>print(q_i)SELECTa.id,a.user_id,a.amount,b.score,b.ageFROMzugh.accountASaINNERJOINzugh.usersASbONa.user_id=b.idWHEREa.id>2>>>q_i.exe()(((3,3,Decimal('299.89'),8,7),(4,4,Decimal('192.10'),8,17)),2)If two underscores are used in the keyword ofwheremethod, the two underscores will be replaced by solid point.
For example,a__idwill be replaced witha.id. This idea was copied from the Django project.We provideTable.inner_join(),Table.left_join()andTable.right_join()methods to support Table join.UnionYou can callunionorunion_allmethod fromSelect object. For example:>>>fromzugh.query.conditionimportgt,lt>>>q_u1=tb.where(id=lt(5)).select()>>>q_u2=tb.where(age=gt(20)).select()>>>q_u=q_u1.union_all(q_u2)>>>print(q_u)SELECT*FROMzugh.usersWHEREid<5UNIONALLSELECT*FROMzugh.usersWHEREage>20Update>>>tb.where(id=1).select().exe()(((1,16,7),),1)>>>q24=tb.where(id=1).update(age=28)>>>print(q24)UPDATEzugh.usersSETage=28WHEREid=1>>>q24.exe()1>>>tb.where(id=1).select().exe()(((1,28,7),),1)F ObjectF object means that it is a field, not a string. It could be used inTable.where()orWhere.update().
F class is a subclass ofArithmeticBase. So F objects can perform mathematical operations, and it will return a newArithmeticBaseinstance.See the following examples:>>>fromzugh.query.othersimportF>>>tb.where(id=1).select().exe()(((1,28,7),),1)>>>q25=tb.where(id=1).update(age=F('age')-2,score=F('score')+6)>>>print(q25)UPDATEzugh.usersSETage=age-2,score=score+6WHEREid=1>>>q25.exe()1>>>tb.where(id=1).select().exe()(((1,26,13),),1)# F object also use in filter>>>q26=tb.where(score=gt(F('age')*2)).select()>>>print(q26)SELECT*FROMzugh.usersWHEREscore>age*2>>>q26.exe()((),0)Delete>>>tb.where(id=5).select().exe()(((5,23,7),),1)>>>q23=tb.where(id=5).delete()>>>print(q23)DELETEFROMzugh.usersWHEREid=5>>>q23.exe()1>>>tb.where(id=5).select().exe()((),0)Decoratordb.query.queryquerydecorator wraps a function which return a Query object. When call the wrapped function,
it would execute a Query object. For example:>>>fromzugh.query.aggregateimportMax>>>fromzugh.db.queryimportquery>>>@query()defquery_max_score():q1=tb.where().select(Max('score'))q2=tb.where(score=In(q1)).select()returnq2>>>query_max_score()(((1,26,13),),1)queryaccept 2 parameters:conn_configandconn_pool. If the Query object returned don't
configure connection, you can pass aconn_configdict  or a connection pool to it.db.query.transactiontransactiondecorator wrap a function which return a list ofQuery ojbect. When call the wrapped
function, it would execute the list ofQuery objectas a transaction. If transaction succeed, return True, otherwise return False.For example:fromzugh.db.queryimporttransactionfromzugh.query.othersimportF@transaction(conn_pool=pool)defmv_score():q1=tb.where(id=3).update(score=F('score')-1)q2=tb.where(id=4).update(score=F('score')+1)return(q1,q2)>>>mv_score()TrueStringSome string function awailable inzugh.query.stringmodule.concat 2 field:fromzugh.query.stringimportConcat,S,Substring>>>q24=tb.where().select(Concat('age','score'))>>>print(q24)SELECTconcat(age,score)FROMzugh.usersS ObjectIn string functions, str meaning field name instead of string. You should useS objectto represent string.fromzugh.query.stringimportConcat,S,Substring>>>q25=tb.where().select(Concat(S('PRI-'),'age'))>>>print(q25)SELECTconcat('PRI-',age)FROMzugh.users>>>q25.exe()((('PRI-26',),('PRI-9',),('PRI-7',),('PRI-17',)),4)>>>print(tb.where().select(Substring('age',2)))SELECTsubstring(age,2)FROMzugh.usersMathSome math functions are awailable inzugh.query.mathmodule.