Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:Ledest:erlang:18
myproto
myproto-0.2.0-git.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File myproto-0.2.0-git.patch of Package myproto
diff --git a/.gitignore b/.gitignore index 16b62b5..69e0294 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ deps *.plt ebin/ log/ -src/mysql.erl +src/mysql_proto.erl +logs/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ee6ca93 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: erlang +script : "./rebar compile && ./rebar skip_deps=true ct" +otp_release: +- 17.4 +# fails to publish SSL information to coveralls (SSL issue in this version) +#- 17.3 +- 17.1 +- 17.0 +notifications: + slack: + secure: R/DFvrTxdCqOtfQ9Fp9x21Dsd74nltDk98LR8Gq/hydJIR8IC6I62b8XLje74YFb0yjtXc6v3AIrZAu8WuLDn6fij+yGV0nRy4z3pZA2Sq9qN1QEZ2MYpRJWy36ZU4qf/ST3ZcUArIQr7hGslpSEJy7f6PhKajcU/5p97Ps3k2s= diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..36aa84e --- /dev/null +++ b/COPYING @@ -0,0 +1,286 @@ +ERLANG PUBLIC LICENSE +Version 1.1 + +1. Definitions. + +1.1. ``Contributor'' means each entity that creates or contributes to +the creation of Modifications. + +1.2. ``Contributor Version'' means the combination of the Original +Code, prior Modifications used by a Contributor, and the Modifications +made by that particular Contributor. + +1.3. ``Covered Code'' means the Original Code or Modifications or the +combination of the Original Code and Modifications, in each case +including portions thereof. + +1.4. ``Electronic Distribution Mechanism'' means a mechanism generally +accepted in the software development community for the electronic +transfer of data. + +1.5. ``Executable'' means Covered Code in any form other than Source +Code. + +1.6. ``Initial Developer'' means the individual or entity identified +as the Initial Developer in the Source Code notice required by Exhibit +A. + +1.7. ``Larger Work'' means a work which combines Covered Code or +portions thereof with code not governed by the terms of this License. + +1.8. ``License'' means this document. + +1.9. ``Modifications'' means any addition to or deletion from the +substance or structure of either the Original Code or any previous +Modifications. When Covered Code is released as a series of files, a +Modification is: + +A. Any addition to or deletion from the contents of a file containing + Original Code or previous Modifications. + +B. Any new file that contains any part of the Original Code or + previous Modifications. + +1.10. ``Original Code'' means Source Code of computer software code +which is described in the Source Code notice required by Exhibit A as +Original Code, and which, at the time of its release under this +License is not already Covered Code governed by this License. + +1.11. ``Source Code'' means the preferred form of the Covered Code for +making modifications to it, including all modules it contains, plus +any associated interface definition files, scripts used to control +compilation and installation of an Executable, or a list of source +code differential comparisons against either the Original Code or +another well known, available Covered Code of the Contributor's +choice. The Source Code can be in a compressed or archival form, +provided the appropriate decompression or de-archiving software is +widely available for no charge. + +1.12. ``You'' means an individual or a legal entity exercising rights +under, and complying with all of the terms of, this License. For legal +entities,``You'' includes any entity which controls, is controlled by, +or is under common control with You. For purposes of this definition, +``control'' means (a) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (b) ownership of fifty percent (50%) or more of the +outstanding shares or beneficial ownership of such entity. + +2. Source Code License. + +2.1. The Initial Developer Grant. +The Initial Developer hereby grants You a world-wide, royalty-free, +non-exclusive license, subject to third party intellectual property +claims: + +(a) to use, reproduce, modify, display, perform, sublicense and + distribute the Original Code (or portions thereof) with or without + Modifications, or as part of a Larger Work; and + +(b) under patents now or hereafter owned or controlled by Initial + Developer, to make, have made, use and sell (``Utilize'') the + Original Code (or portions thereof), but solely to the extent that + any such patent is reasonably necessary to enable You to Utilize + the Original Code (or portions thereof) and not to any greater + extent that may be necessary to Utilize further Modifications or + combinations. + +2.2. Contributor Grant. +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license, subject to third party intellectual property +claims: + +(a) to use, reproduce, modify, display, perform, sublicense and + distribute the Modifications created by such Contributor (or + portions thereof) either on an unmodified basis, with other + Modifications, as Covered Code or as part of a Larger Work; and + +(b) under patents now or hereafter owned or controlled by Contributor, + to Utilize the Contributor Version (or portions thereof), but + solely to the extent that any such patent is reasonably necessary + to enable You to Utilize the Contributor Version (or portions + thereof), and not to any greater extent that may be necessary to + Utilize further Modifications or combinations. + +3. Distribution Obligations. + +3.1. Application of License. +The Modifications which You contribute are governed by the terms of +this License, including without limitation Section 2.2. The Source +Code version of Covered Code may be distributed only under the terms +of this License, and You must include a copy of this License with +every copy of the Source Code You distribute. You may not offer or +impose any terms on any Source Code version that alters or restricts +the applicable version of this License or the recipients' rights +hereunder. However, You may include an additional document offering +the additional rights described in Section 3.5. + +3.2. Availability of Source Code. +Any Modification which You contribute must be made available in Source +Code form under the terms of this License either on the same media as +an Executable version or via an accepted Electronic Distribution +Mechanism to anyone to whom you made an Executable version available; +and if made available via Electronic Distribution Mechanism, must +remain available for at least twelve (12) months after the date it +initially became available, or at least six (6) months after a +subsequent version of that particular Modification has been made +available to such recipients. You are responsible for ensuring that +the Source Code version remains available even if the Electronic +Distribution Mechanism is maintained by a third party. + +3.3. Description of Modifications. +You must cause all Covered Code to which you contribute to contain a +file documenting the changes You made to create that Covered Code and +the date of any change. You must include a prominent statement that +the Modification is derived, directly or indirectly, from Original +Code provided by the Initial Developer and including the name of the +Initial Developer in (a) the Source Code, and (b) in any notice in an +Executable version or related documentation in which You describe the +origin or ownership of the Covered Code. + +3.4. Intellectual Property Matters + +(a) Third Party Claims. + If You have knowledge that a party claims an intellectual property + right in particular functionality or code (or its utilization + under this License), you must include a text file with the source + code distribution titled ``LEGAL'' which describes the claim and + the party making the claim in sufficient detail that a recipient + will know whom to contact. If you obtain such knowledge after You + make Your Modification available as described in Section 3.2, You + shall promptly modify the LEGAL file in all copies You make + available thereafter and shall take other steps (such as notifying + appropriate mailing lists or newsgroups) reasonably calculated to + inform those who received the Covered Code that new knowledge has + been obtained. + +(b) Contributor APIs. + If Your Modification is an application programming interface and + You own or control patents which are reasonably necessary to + implement that API, you must also include this information in the + LEGAL file. + +3.5. Required Notices. +You must duplicate the notice in Exhibit A in each file of the Source +Code, and this License in any documentation for the Source Code, where +You describe recipients' rights relating to Covered Code. If You +created one or more Modification(s), You may add your name as a +Contributor to the notice described in Exhibit A. If it is not +possible to put such notice in a particular Source Code file due to +its structure, then you must include such notice in a location (such +as a relevant directory file) where a user would be likely to look for +such a notice. You may choose to offer, and to charge a fee for, +warranty, support, indemnity or liability obligations to one or more +recipients of Covered Code. However, You may do so only on Your own +behalf, and not on behalf of the Initial Developer or any +Contributor. You must make it absolutely clear than any such warranty, +support, indemnity or liability obligation is offered by You alone, +and You hereby agree to indemnify the Initial Developer and every +Contributor for any liability incurred by the Initial Developer or +such Contributor as a result of warranty, support, indemnity or +liability terms You offer. + +3.6. Distribution of Executable Versions. +You may distribute Covered Code in Executable form only if the +requirements of Section 3.1-3.5 have been met for that Covered Code, +and if You include a notice stating that the Source Code version of +the Covered Code is available under the terms of this License, +including a description of how and where You have fulfilled the +obligations of Section 3.2. The notice must be conspicuously included +in any notice in an Executable version, related documentation or +collateral in which You describe recipients' rights relating to the +Covered Code. You may distribute the Executable version of Covered +Code under a license of Your choice, which may contain terms different +from this License, provided that You are in compliance with the terms +of this License and that the license for the Executable version does +not attempt to limit or alter the recipient's rights in the Source +Code version from the rights set forth in this License. If You +distribute the Executable version under a different license You must +make it absolutely clear that any terms which differ from this License +are offered by You alone, not by the Initial Developer or any +Contributor. You hereby agree to indemnify the Initial Developer and +every Contributor for any liability incurred by the Initial Developer +or such Contributor as a result of any such terms You offer. + +3.7. Larger Works. +You may create a Larger Work by combining Covered Code with other code +not governed by the terms of this License and distribute the Larger +Work as a single product. In such a case, You must make sure the +requirements of this License are fulfilled for the Covered Code. + +4. Inability to Comply Due to Statute or Regulation. +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Code due to statute +or regulation then You must: (a) comply with the terms of this License +to the maximum extent possible; and (b) describe the limitations and +the code they affect. Such description must be included in the LEGAL +file described in Section 3.4 and must be included with all +distributions of the Source Code. Except to the extent prohibited by +statute or regulation, such description must be sufficiently detailed +for a recipient of ordinary skill to be able to understand it. + +5. Application of this License. + +This License applies to code to which the Initial Developer has +attached the notice in Exhibit A, and to related Covered Code. + +6. CONNECTION TO MOZILLA PUBLIC LICENSE + +This Erlang License is a derivative work of the Mozilla Public +License, Version 1.0. It contains terms which differ from the Mozilla +Public License, Version 1.0. + +7. DISCLAIMER OF WARRANTY. + +COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN ``AS IS'' BASIS, +WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF +DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR +NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF +THE COVERED CODE IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE +IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER +CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR +CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART +OF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER +EXCEPT UNDER THIS DISCLAIMER. + +8. TERMINATION. +This License and the rights granted hereunder will terminate +automatically if You fail to comply with terms herein and fail to cure +such breach within 30 days of becoming aware of the breach. All +sublicenses to the Covered Code which are properly granted shall +survive any termination of this License. Provisions which, by their +nature, must remain in effect beyond the termination of this License +shall survive. + +9. DISCLAIMER OF LIABILITY +Any utilization of Covered Code shall not cause the Initial Developer +or any Contributor to be liable for any damages (neither direct nor +indirect). + +10. MISCELLANEOUS +This License represents the complete agreement concerning the subject +matter hereof. If any provision is held to be unenforceable, such +provision shall be reformed only to the extent necessary to make it +enforceable. This License shall be construed by and in accordance with +the substantive laws of Sweden. Any dispute, controversy or claim +arising out of or relating to this License, or the breach, termination +or invalidity thereof, shall be subject to the exclusive jurisdiction +of Swedish courts, with the Stockholm City Court as the first +instance. + +EXHIBIT A. + +``The contents of this file are subject to the Erlang Public License, +Version 1.1, (the "License"); you may not use this file except in +compliance with the License. You should have received a copy of the +Erlang Public License along with this software. If not, it can be +retrieved via the world wide web at http://www.erlang.org/. + +Software distributed under the License is distributed on an "AS IS" +basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +the License for the specific language governing rights and limitations +under the License. + +The Initial Developer of the Original Code is Ericsson Utvecklings AB. +Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings +AB. All Rights Reserved.'' diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0665f0a --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ + +all: deps/neotoma/ebin/neotoma.beam + ./rebar compile skip_deps=true + +deps/neotoma/ebin/neotoma.beam: + ./rebar get-deps compile + +clean: + ./rebar clean skip_deps=true + rm -f src/mysql_proto.erl + +test: deps/neotoma/ebin/neotoma.beam + ./rebar ct skip_deps=true + +.PHONY: test diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f7ef0f --- /dev/null +++ b/README.md @@ -0,0 +1,216 @@ +Overview +======== + +[![Build Status](https://api.travis-ci.org/altenwald/myproto.png)](https://travis-ci.org/altenwald/myproto) + +MySQL Server Protocol in Erlang. This project let you implement the MySQL protocol for your server. Throught a MySQL connection you could send queries or fake a MySQL connection to do a proxy or whatever else. + +Requirements +------------ + +The system for tests use maps so, you have to use Erlang OTP 17+. + +Usage +----- + +If you want to use, only add this in rebar.config: + +```erlang + {deps, [ + {myproto, ".*", {git, "git://github.com/altenwald/myproto.git", master}} + ]}. +``` + +Configuration +------------- + +If you want configure the port, the handler and the server sign: + +```erlang + {myproto, [ + {handler, my_dummy_handler}, + {parse_query, true}, + {server_sign, <<"5.5-myproto">>}, + {default_storage_engine, <<"myproto">>}, + {port, 3306} + ]}. +``` + +A dummy test +------------ + +This is a little tutorial to show you the use of this library. As a little introduction, you can create your environment as follow: + +```shell +mkdir -p mydummy/apps/mydummy +cd mydummy/apps/mydummy +rebar create template=simpleapp appid=mydummy +cd ../.. + +mkdir rel +cd rel +rebar create template=simplenode nodeid=mydummy +cd .. +``` + +Create the file rebar.config as follow: + +```erlang +{sub_dirs, ["apps/*", "rel"]}. +{deps, [ + {myproto, ".*", {git, "git://github.com/altenwald/myproto.git", master}} +]}. +``` + +Modify the file `rel/files/sys.config` as: + +```erlang +[ + {myproto, [ + {handler, mydummy}, + {parse_query, true}, + {server_sign, <<"5.5-myproto">>}, + {default_storage_engine, <<"myproto">>}, + {port, 3306} + ]}, + + %% SASL config + {sasl, [ + {sasl_error_logger, {file, "log/sasl-error.log"}}, + {errlog_type, error}, + {error_logger_mf_dir, "log/sasl"}, % Log directory + {error_logger_mf_maxbytes, 10485760}, % 10 MB max file size + {error_logger_mf_maxfiles, 5} % 5 files max + ]} +]. +``` + +Now, you can create the `apps/mydummy/src/mydummy.erl` module: + +```erlang +-module(mydummy). +-behaviour(gen_myproto). + +-include_lib("myproto/include/myproto.hrl"). + +-export([check_pass/3, metadata/2, execute/2, terminate/2]). + +-record(my, { + db +}). + +check_pass(#user{name = User, server_hash = Hash, password = Password}) -> + case my_request:check_clean_pass(User, Hash) of + HashedPassword -> {ok, HashedPassword, #my{}}; + _ -> {error, <<"Password incorrect!">>} + end. + + +metadata(version, State) -> + {reply, <<"my cool server 1.0">>, State}; + +metadata({connect_db, Database}, State) -> + {noreply, State#my{db = Database}}; + +metadata(databases, State) -> + {reply, [<<"comet">>], State}; + +metadata(tables, #db{db = <<"storage">>} = State) -> + {reply, {<<"storage">>, [<<"channels">>, <<"users">>]}, State}; + +metadata({fields, <<"channels">> = Table}, #db{db = <<"storage">> = DB} = State) -> + {reply, {DB, Table, [{name,string},{users_count,integer},{started_at,integer}]}, State}; + +metadata(_, #my{} = State) -> + {noreply, State}. + + +execute(#request{info = + #select{params=[#variable{name = <<"version_comment">>}] + }}, State) -> + Info = { + [#column{name = <<"@@version_comment">>, type=?TYPE_VARCHAR, length=20}], + [[<<"myproto 0.1">>]] + }, + {reply, #response{status=?STATUS_OK, info=Info}, State}; + +execute(#request{command = ?COM_QUIT}, State) -> + lager:info("Exiting~n", []), + {stop, normal, State}; + +execute(#request{info = #select{}} = Request, State) -> + lager:info("Request: ~p~n", [Request]), + Info = { + [ + #column{name = <<"Info">>, type=?TYPE_VARCHAR, length=20}, + #column{name = <<"Info2">>, type=?TYPE_VARCHAR, length=20} + ], + [ + [<<"Not implemented!">>, <<"Yet">>], + [<<"Testing MultiColumn!">>, <<"Still">>] + ] + }, + {reply, #response{status=?STATUS_OK, info=Info}, State}; + +execute(#request{} = Request, State) -> + lager:info("Unknown request: ~p", [Request]), + {reply, default, State}. % Return default reply if you don't know answer on this request + + +terminate(_Reason, _State) -> + ok. +``` + +Please, mention that dummy module must implement metadata callback, because usually mysql clients ask a lot of information on start about databases, tables and fields in them. + +For example, mysql has three different protocols for querying table structure. myproto hides this stuff from you in a very simple and convenient way. + +Add this to `rel/reltool.config`: + +```erlang +{sys, + {lib_dirs, ["../apps", "../deps"]}, + {erts, [{mod_cond, derived}, {app_file, strip}]}, + {app_file, strip}, + {rel, "mydummy", "1", [ + kernel, + stdlib, + sasl, + inets, + lager, + myproto, + mydummy + ]}, + ... +``` + +Now time to build: + +```shell +rebar get-deps compile generate +``` + +And time to launch: + +```shell +rel/mydummy/bin/mydummy console +``` + +Now, to test, in another terminal or shell: + +```shell +mysql -uroot -proot -h127.0.0.1 +``` + +You can use some SQL statements as: + +```sql +SELECT @@version_comment; +SELECT * FROM sometable; +``` + +Done :-) + + +[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/altenwald/myproto/trend.png)](https://bitdeli.com/free "Bitdeli Badge") + diff --git a/README.org b/README.org deleted file mode 100644 index 4855290..0000000 --- a/README.org +++ /dev/null @@ -1,26 +0,0 @@ -* Overview - - MySQL Server Protocol in Erlang. This project let you implement the MySQL protocol for your server. Throught a MySQL connection you could send queries or fake a MySQL connection to do a proxy or whatever else. - -* Usage - - If you want to use, only add this in rebar.config: - -#+BEGIN_EXAMPLE - {deps, [ - {myproto, ".*", {git, "git://github.com/bosqueviejo/myproto.git", master}} - ]}. -#+END_EXAMPLE - -* Configuration - - If you want configure the port, the handler and the server sign: - -#+BEGIN_EXAMPLE - {myproto, [ - {handler, my_dummy_handler}, - {parse_query, true}, - {server_sign, <<"5.5-myproto">>}, - {port, 3306} - ]}. -#+END_EXAMPLE diff --git a/include/myproto.hrl b/include/myproto.hrl index ab4d64e..2209768 100644 --- a/include/myproto.hrl +++ b/include/myproto.hrl @@ -1,4 +1,4 @@ --define(SERVER_SIGN, "5.5-myproto"). +-define(SERVER_SIGN, <<"5.5-myproto">>). %% status flags -define(SERVER_STATUS_IN_TRANS, 16#0001). @@ -121,27 +121,32 @@ -define(CLIENT_CONNECT_ATTRS, 16#100000). -define(CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA, 16#200000). --include("sql.hrl"). +-define(DATA_NULL, (<<16#fb>>)). --record(request, { - command :: integer(), - info :: string() | sql(), - continue = false :: boolean(), - id = 0 :: integer() -}). - --type request() :: #request{}. +-include("sql.hrl"). -record(user, { name :: binary(), password :: binary(), capabilities :: integer(), plugin :: binary(), - charset :: binary() + charset :: binary(), + database :: binary(), + server_hash :: binary() }). -type user() :: #user{}. +-record(request, { + command :: integer(), + info :: binary() | sql() | user(), + text :: binary(), + continue = false :: boolean(), + id = 0 :: integer() +}). + +-type request() :: #request{}. + -record(response, { status = 0 :: integer(), id = 0 :: integer(), @@ -159,6 +164,8 @@ -record(column, { schema = <<>> :: binary(), table = <<>> :: binary(), + org_name = <<>> :: binary(), + org_table = <<>> :: binary(), name :: binary(), charset = ?UTF8_GENERAL_CI :: integer(), length :: integer(), @@ -174,3 +181,4 @@ -type password() :: binary(). -type hash() :: binary(). +-type state() :: term(). diff --git a/include/sql.hrl b/include/sql.hrl index 9350d57..424ba30 100644 --- a/include/sql.hrl +++ b/include/sql.hrl @@ -11,8 +11,10 @@ -record(operation, {type, op1, op2}). -record(variable, {name, label, scope}). +-record(system_set, {'query'}). + % SHOW --record(show, {type, full, from}). +-record(show, {type, full, from, conditions}). -type show() :: #show{}. @@ -36,6 +38,18 @@ % INSERT -record(insert, {table, values}). +% DESCRIBE +-record(describe, {table}). + -type insert() :: #insert{}. -type sql() :: show() | select() | update() | delete() | insert(). + +% Database Administration Statements +-record(management, {action :: action(), data :: account() | permission() }). +-record(account, {access}). +-record(permission, {on, account, conditions}). + +-type action() :: create | drop | grant | rename | revoke | setpasswd. +-type account() :: #account{}. +-type permission() :: #permission{}. diff --git a/nanomysql.erl b/nanomysql.erl new file mode 100755 index 0000000..5d6e298 --- /dev/null +++ b/nanomysql.erl @@ -0,0 +1,61 @@ +#!/usr/bin/env ERL_LIBS=deps escript + +-mode(compile). + +main([URL|_Args]) -> + code:add_pathz("ebin"), + {ok, Sock} = nanomysql:connect(URL), + {ok, {mysql, _, _Host, _Port, "/"++_DBName, _}} = http_uri:parse(URL, [{scheme_defaults,[{mysql,3306}]}]), + % DB = list_to_binary(DBName), + % nanomysql:execute("show databases", Sock), + % {ok, {_, Rows}} = nanomysql:execute("show tables", Sock), + % [nanomysql:command(4, <<Name/binary,0>>, Sock) || [Name] <- Rows], + loop(Sock); + +main([]) -> + io:format("~s mysql://user:password@host:port/dbname\n", [escript:script_name()]), + erlang:halt(2). + + +loop(Sock) -> + case io:get_line("mysql> ") of + "\\?\n" -> help(); + "\\d "++Name1 -> + Name = string:substr(Name1,1, length(Name1) -1), + {ok, Reply} = nanomysql:command(show_fields, iolist_to_binary([Name,0]), Sock), + print_reply(Reply); + "exit\n" -> halt(0); + "quit\n" -> halt(0); + Query -> + {ok, Reply} = nanomysql:execute(Query, Sock), + print_reply(Reply) + end, + loop(Sock). + + +help() -> + io:format( +"Informational:\n" +% " \\d list tables\n" +" \\d NAME describe table\n" +% " \\l list databases\n" +). + +print_reply(#{affected_rows := _} = Info) -> + io:format("info ~p\n", [Info]); + +print_reply({Columns}) -> + print_columns(Columns), + io:format("\nok\n"); + +print_reply({Columns, Rows}) -> + print_columns(Columns), + print_rows(Rows), + io:format("\nok\n"). + +print_columns(Columns) -> + io:format("~s\n---\n", [ string:join([io_lib:format("~s(~p)",[C,T]) || {C,T} <- Columns], ",")]). + +print_rows(Rows) -> + [io:format("~s\n", [ string:join([io_lib:format("~p",[C]) || C <- Row],",")]) || Row <- Rows]. + diff --git a/rebar b/rebar new file mode 100755 index 0000000..9fb8a79 Binary files /dev/null and b/rebar differ diff --git a/rebar.config b/rebar.config index 341d9f1..9aaf1ec 100644 --- a/rebar.config +++ b/rebar.config @@ -1,9 +1,13 @@ {erl_opts, [{parse_transform, lager_transform}]}. +{lib_dirs, ["deps"]}. {deps, [ - {lager, ".*", {git, "git://github.com/basho/lager.git", master}}, + {lager, ".*", {git, "git://github.com/basho/lager.git", {tag, "2.0.2"}}}, + {ranch, ".*", {git, "git://github.com/extend/ranch.git", {tag, "1.0.0"}}}, {neotoma, ".*", {git, "git://github.com/seancribbs/neotoma.git", master}} ]}. {eunit_opts, [verbose, {report,{eunit_surefire,[{dir,"."}]}}]}. -{cover_enabled, true}. -{cover_print_enable, true}. +%{cover_enabled, true}. +%{cover_print_enable, true}. + + diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..272f244 --- /dev/null +++ b/run.sh @@ -0,0 +1,5 @@ +#!/bin/sh + + +ERL_LIBS=deps erl -sname mysql -pa ebin -s lager -s myproto + diff --git a/src/gen_myproto.erl b/src/gen_myproto.erl index ac906b4..2c72f05 100644 --- a/src/gen_myproto.erl +++ b/src/gen_myproto.erl @@ -1,25 +1,40 @@ -module(gen_myproto). +-author('Manuel Rubio <manuel@altenwald.com>'). --include("../include/myproto.hrl"). +-include("myproto.hrl"). -callback check_pass( - User::user_string(), - Hash::hash(), - Password::password() + #user{} ) -> - {ok, password(), State::term()} | - {error, Reason::binary()} | - {error, Code::integer(), Reason::binary()}. + {ok, password(), State::term()} | + {error, Reason::binary()} | + {error, Code::integer(), Reason::binary()}. + + +-type metadata() :: + {connect_db, binary()} | + databases | + tables | + version. + + +-callback metadata(metadata(), state()) -> + {reply, Value::any(), state()} | + {error, Reason::any(), state()} | + {noreply, state()}. -callback execute( - Query :: request(), - State :: term() + Query :: request(), + state() ) -> - {#response{}, State::term()}. + {reply, default, state()} | + {reply, response(), state()} | + {noreply, state()} | + {stop, Reason::term(), state()}. -callback terminate( - Reason :: term(), - State :: term() + Reason :: term(), + state() ) -> - ok. + ok. diff --git a/src/my_acceptor.erl b/src/my_acceptor.erl index afc4fc2..3fbe11a 100644 --- a/src/my_acceptor.erl +++ b/src/my_acceptor.erl @@ -1,5 +1,5 @@ -module(my_acceptor). --author('bombadil@bosqueviejo.net'). +-author('Manuel Rubio <manuel@altenwald.com>'). -behaviour(gen_server). diff --git a/src/my_datatypes.erl b/src/my_datatypes.erl index 46acb28..e4e4dad 100644 --- a/src/my_datatypes.erl +++ b/src/my_datatypes.erl @@ -1,45 +1,56 @@ -module(my_datatypes). +-author('Manuel Rubio <manuel@altenwald.com>'). -export([ - string_nul_to_binary/1, - binary_to_varchar/1, - fix_integer_to_number/2, number_to_fix_integer/2, - var_integer_to_number/1, number_to_var_integer/1 + string_nul_to_binary/1, + binary_to_varchar/1, + fix_integer_to_number/2, number_to_fix_integer/2, + var_integer_to_number/1, number_to_var_integer/1, + read_lenenc_string/1 ]). --include("../include/myproto.hrl"). +-include("myproto.hrl"). + + +read_lenenc_string(<<16#fc, Len:16/little, Bin:Len/binary, Rest/binary>>) -> {Bin, Rest}; +read_lenenc_string(<<16#fd, Len:24/little, Bin:Len/binary, Rest/binary>>) -> {Bin, Rest}; +read_lenenc_string(<<16#fe, Len:64/little, Bin:Len/binary, Rest/binary>>) -> {Bin, Rest}; +read_lenenc_string(<<Len:8/little, Bin:Len/binary, Rest/binary>>) -> {Bin, Rest}. + %% String.NUL -spec string_nul_to_binary(String :: binary()) -> binary(). string_nul_to_binary(String) -> - list_to_binary(lists:takewhile(fun(X) -> - X =/= 0 - end, binary_to_list(String))). + list_to_binary(lists:takewhile(fun(X) -> + X =/= 0 + end, binary_to_list(String))). %% Varchars --spec binary_to_varchar(Binary::binary()) -> binary(). +-spec binary_to_varchar(Binary::binary() | null) -> binary(). +binary_to_varchar(null) -> + <<16#fb>>; binary_to_varchar(Binary) -> - Len = number_to_var_integer(byte_size(Binary)), - <<Len/binary, Binary/binary>>. + Len = number_to_var_integer(byte_size(Binary)), + <<Len/binary, Binary/binary>>. %% Fix Integers -spec fix_integer_to_number(Size::integer(), Data::integer()) -> integer(). -fix_integer_to_number(Size, Data) when is_integer(Size) andalso is_integer(Data) -> - BitSize = Size * 8, - <<Num:BitSize/little>> = Data, - Num. +fix_integer_to_number(Size, Data) when is_integer(Size) andalso is_binary(Data) -> + BitSize = Size * 8, + <<Num:BitSize/little>> = Data, + Num. -spec number_to_fix_integer(Size::integer(), Data::binary()) -> binary(). -number_to_fix_integer(Size, Data) when is_integer(Size) andalso is_binary(Data) -> - BitSize = Size * 8, - <<Data:BitSize/little>>. +number_to_fix_integer(Size, Data) when is_integer(Size) andalso is_integer(Data) -> + BitSize = Size * 8, + <<Data:BitSize/little>>. %% Var integers @@ -53,11 +64,11 @@ var_integer_to_number(<<Data:8/little>>) -> Data. -spec number_to_var_integer(Data::integer()) -> binary(). number_to_var_integer(Data) when is_integer(Data) andalso Data < 251 -> - <<Data:8>>; + <<Data:8>>; number_to_var_integer(Data) when is_integer(Data) andalso Data < 16#10000 -> - <<16#fc, Data:16/little>>; + <<16#fc, Data:16/little>>; number_to_var_integer(Data) when is_integer(Data) andalso Data < 16#1000000 -> - <<16#fd, Data:24/little>>; + <<16#fd, Data:24/little>>; number_to_var_integer(Data) when is_integer(Data) -> - <<16#fe, Data:64/little>>. + <<16#fe, Data:64/little>>. diff --git a/src/my_dummy_handler.erl b/src/my_dummy_handler.erl index 923c5bf..a7446bf 100644 --- a/src/my_dummy_handler.erl +++ b/src/my_dummy_handler.erl @@ -1,22 +1,66 @@ -module(my_dummy_handler). +-author('Manuel Rubio <manuel@altenwald.com>'). +-author('Max Lapshin <max@maxidoors.ru>'). + -behaviour(gen_myproto). --include("../include/myproto.hrl"). +-include("myproto.hrl"). --export([check_pass/3, execute/2, terminate/2]). +-export([check_pass/1, execute/2, metadata/2, terminate/2]). -check_pass(User, Hash, Password) -> +check_pass(#user{name = User, server_hash = Hash, password = Password}) -> case my_request:check_clean_pass(User, Hash) of Password -> {ok, Password, []}; _ -> {error, <<"Password incorrect!">>} end. + +metadata(version, State) -> + {reply, <<"dummy handler 1.0">>, State}; + +metadata({connect_db, _Database}, State) -> + {noreply, State}; + +metadata(databases, State) -> + {reply, [<<"myfakedatabase">>], State}; + +metadata(_, State) -> + {noreply, State}. + + execute(#request{info = #select{params=[#variable{name = <<"version_comment">>}]}}, State) -> Info = { [#column{name = <<"@@version_comment">>, type=?TYPE_VARCHAR, length=20}], [[<<"myproto 0.1">>]] }, - {#response{status=?STATUS_OK, info=Info}, State}; + {reply, #response{status=?STATUS_OK, info=Info}, State}; + +execute(#request{command = ?COM_QUIT}, State) -> + lager:info("Exiting~n", []), + {stop, normal, State}; + +execute(#request{command = ?COM_INIT_DB, info=Database}, State) -> + lager:info("Change database to: ~p~n", [Database]), + {reply, #response { + status=?STATUS_OK, info = <<"Changed to ", Database/binary>>, + affected_rows = 0, last_insert_id = 0, + status_flags = 0, warnings = 0 + }, State}; + +execute(#request{command=?COM_FIELD_LIST, id=_Id, info=_Table}, State) -> + {reply, #response{ + status=?STATUS_ERR, error_code=2003, %% TODO: found the correct error code + info = <<"Not implemented field list">> + }, State}; + +execute(#request{command = ?COM_QUERY, info={use, Database}}, State) -> + lager:info("Change database to: ~p~n", [Database]), + {reply, #response { + status=?STATUS_OK, info = <<"Changed to ", Database/binary>>, + affected_rows = 0, last_insert_id = 0, + status_flags = 0, warnings = 0 + }, State}; + execute(Request, State) -> lager:info("Request: ~p~n", [Request]), Info = { @@ -29,7 +73,8 @@ execute(Request, State) -> [<<"Testing MultiColumn!">>, <<"Still">>] ] }, - {#response{status=?STATUS_OK, info=Info}, State}. + {reply, #response{status=?STATUS_OK, info=Info}, State}. + terminate(_Reason, _State) -> ok. diff --git a/src/my_packet.erl b/src/my_packet.erl index 9b22b95..1317bef 100644 --- a/src/my_packet.erl +++ b/src/my_packet.erl @@ -1,8 +1,10 @@ -module(my_packet). +-author('Manuel Rubio <manuel@altenwald.com>'). +-author('Max Lapshin <max@maxidoors.ru>'). -export([encode/1, decode/1, decode_auth/1]). --include("../include/myproto.hrl"). +-include("myproto.hrl"). encode(#response{ status=?STATUS_EOF, id=Id, warnings=Warnings, @@ -19,7 +21,8 @@ encode(#response{ error_info = Code, info = Info }) when Code =/= <<"">> -> Length = byte_size(Info) + 9, - <<Length:24/little, Id:8, ?STATUS_ERR:8, Error:16/little, "#", Code:5/binary, Info/binary>>; + <<Length:24/little, Id:8, ?STATUS_ERR:8, Error:16/little, "#", Code:5/binary, + Info/binary>>; encode(#response{ status=?STATUS_ERR, id=Id, error_code=Error, info = Info @@ -27,6 +30,18 @@ encode(#response{ Length = byte_size(Info) + 3, <<Length:24/little, Id:8, ?STATUS_ERR:8, Error:16/little, Info/binary>>; encode(#response{ + status=?STATUS_OK, id=Id, info={Cols} + }) -> + %% columns + {IdEof, ColsBin} = encode_column(Cols, Id), + %% eof + ColsEof = encode(#response{ + status=?STATUS_EOF, + id=IdEof, + status_flags=?SERVER_STATUS_AUTOCOMMIT + }), + <<ColsBin/binary, ColsEof/binary>>; +encode(#response{ status=?STATUS_OK, id=Id, info={Cols, Rows} }) -> %% Column account @@ -41,7 +56,7 @@ encode(#response{ status_flags=?SERVER_STATUS_AUTOCOMMIT }), %% rows - {IdEnd, RowsPack} = encode_row(Rows, IdEof+1), + {IdEnd, RowsPack} = encode_rows(Rows, Cols, IdEof+1), %% eof RowsEof = encode(#response{ status=?STATUS_EOF, @@ -65,6 +80,7 @@ encode(#response{ encode(#response{ status=?STATUS_HELLO, id=Id, info=Hash }) -> + 20 == size(Hash) orelse error({invalid_hash_size,size(Hash),need,20}), ServerSign = case application:get_env(myproto, server_sign) of {ok, SS} when is_binary(SS) -> SS; {ok, SS} when is_list(SS) -> list_to_binary(SS); @@ -76,8 +92,7 @@ encode(#response{ ?CLIENT_SECURE_CONNECTION bor %% for mysql_native_password 0, <<CapsLow:16/little, CapsUp:16/little>> = <<Caps:32/little>>, - <<IntHash:160/little-unsigned-integer>> = Hash, - <<Auth1:8/binary, Auth2/binary>> = <<IntHash:160/little-unsigned-integer>>, + <<Auth1:8/binary, Auth2/binary>> = Hash, LenAuth = 21, StatusFlags = ?SERVER_STATUS_AUTOCOMMIT bor @@ -100,19 +115,31 @@ encode_column(Cols, Id) when is_list(Cols) -> end, {Id, <<"">>}, Cols); encode_column(#column{ schema = Schema, table = Table, name = Name, - charset = Charset, length = Length, type = Type, - flags = Flags, decimals = Decimals - }, Id) -> + charset = Charset, length = L, type = Type, + flags = Flags, decimals = Decimals, org_name = ON + }=_Column, Id) when is_binary(Schema), is_binary(Table), is_binary(Name), + is_integer(Charset), is_integer(Type), is_integer(Flags), + is_integer(Decimals) -> SchemaLen = my_datatypes:number_to_var_integer(byte_size(Schema)), TableLen = my_datatypes:number_to_var_integer(byte_size(Table)), NameLen = my_datatypes:number_to_var_integer(byte_size(Name)), + {OrgNameLen, OrgName} = case ON of + undefined -> {NameLen, bin_to_upper(Name)}; + ON -> {my_datatypes:number_to_var_integer(byte_size(ON)), ON} + end, + Length = case {Type, L} of + _ when is_integer(L) -> L; + {?TYPE_DATETIME,undefined} -> 16#13; + {?TYPE_LONGLONG,undefined} -> 16#15; + {_,undefined} -> 0 + end, Payload = << 3:8, "def", SchemaLen/binary, Schema/binary, TableLen/binary, Table/binary, % table TableLen/binary, Table/binary, % org_table NameLen/binary, Name/binary, % name - NameLen/binary, Name/binary, % org_name + OrgNameLen/binary, OrgName/binary, % org_name 16#0c:8, Charset:16/little, Length:32/little, Type:8, Flags:16/little, Decimals:8/little, @@ -121,57 +148,169 @@ encode_column(#column{ PayloadLen = byte_size(Payload), <<PayloadLen:24/little, Id:8, Payload/binary>>. -encode_row(Rows, Id) -> - lists:foldl(fun(Cols, {NewId, Data}) -> - Payload = lists:foldl(fun(Cell, Col) -> - CellEnc = my_datatypes:binary_to_varchar(Cell), - <<Col/binary, CellEnc/binary>> - end, <<"">>, Cols), +encode_rows(Rows, Cols, Id) -> + lists:foldl(fun(Values, {NewId, Data}) -> + Payload = lists:foldl(fun({#column{type = Type, name = Name}, Cell}, Binary) -> + Cell1 = case Type of + _ when Cell == undefined -> undefined; + _ when Cell == true -> <<"1">>; + _ when Cell == false -> <<"0">>; + T when (T == ?TYPE_TINY orelse + T == ?TYPE_SHORT orelse + T == ?TYPE_LONG orelse + T == ?TYPE_LONGLONG orelse + T == ?TYPE_INT24 orelse + T == ?TYPE_YEAR) andalso is_integer(Cell) -> + integer_to_binary(Cell); + _ when is_binary(Cell) -> Cell; + _ -> error({cannot_encode,Name,Type,Cell}) + end, + CellEnc = case Cell of + undefined -> ?DATA_NULL; + _ -> my_datatypes:binary_to_varchar(Cell1) + end, + <<Binary/binary, CellEnc/binary>> + end, <<"">>, lists:zip(Cols,Values)), Length = byte_size(Payload), {NewId+1, <<Data/binary, Length:24/little, NewId:8, Payload/binary>>} end, {Id, <<"">>}, Rows). -decode_auth(<< - _Length:24/little, 1:8, Caps:32/little, _MaxPackSize:32/little, Charset:8, - _Reserved:23/binary, Info/binary ->>) -> - case binary:split(Info, <<0>>) of - [User, <<20:8, Password:20/binary, PlugIn/binary>>] -> - UserData = #user{ - name=User, - password=Password, - plugin=PlugIn, - capabilities=Caps, - charset=Charset - }; - [User, <<0:8, PlugIn/binary>>] -> - UserData = #user{ - name=User, - password=undefined, - plugin=PlugIn, - capabilities=Caps, - charset=Charset - } +-spec decode_auth(binary()) -> {ok, user(), binary()} | {more, binary()}. + +decode_auth(<<Length:24/little, 1:8, Bin:Length/binary, Rest/binary>>) -> + {ok, decode_auth0(Bin), Rest}; + +decode_auth(<<Length:24/little, 1:8, Bin/binary>>) -> + {more, size(Bin) - Length}; + +decode_auth(<<Bin/binary>>) -> + {more, 4 - size(Bin)}. + +-spec decode_auth0(Auth::binary()) -> request(). + +decode_auth0(<<CapsFlag:32/little, _MaxPackSize:32/little, Charset:8, + _Reserved:23/binary, Info0/binary>>) -> + Caps = unpack_caps(CapsFlag), + {User, Info1} = unpack_zero(Info0), + + {Password,Info2} = case proplists:get_value(auth_lenenc_client_data,Caps) of + true -> my_datatypes:read_lenenc_string(Info1); + _ -> + case proplists:get_value(secure_connection, Caps) of + true -> + <<PassLen, Pass:PassLen/binary, R/binary>> = Info1, + {Pass, R}; + false -> + unpack_zero(Info1) + end + end, + + HasPluginAuth = proplists:get_value(plugin_auth, Caps), + {DB, Info3} = case proplists:get_value(connect_with_db, Caps) of + % For some strange reasons mysql 5.0.6 violates protocol and doesn't + % send db name + % http://dev.mysql.com/doc/internals/en/connection-phase-packets.html + % #packet-Protocol::HandshakeResponse41 + % so we write here a dirty hack for pymysql + true when HasPluginAuth == undefined andalso size(Info2) > 0 -> + unpack_zero(Info2); + _ -> + {undefined, Info2} + end, + {Plugin, _Info4} = case proplists:get_value(plugin_auth, Caps) of + true -> + unpack_zero(Info3); + _ -> + {undefined, Info3} end, + UserData = #user{ + name=User, + password=Password, + plugin=Plugin, + capabilities=Caps, + database=DB, + charset=Charset + }, #request{command=?COM_AUTH, info=UserData}. -decode(<<_Length:24/little, Id:8, ?COM_FIELD_LIST:8, Info/binary>>) -> +-spec unpack_zero(String::binary()) -> {B1::binary(), B2::binary()}. + +unpack_zero(String) -> + [B1, B2] = binary:split(String, <<0>>), + {B1, B2}. + +-spec unpack_caps(Flag::integer()) -> [atom()]. + +unpack_caps(Flag) -> + Caps = [ + {?CLIENT_LONG_PASSWORD,long_password}, + {?CLIENT_FOUND_ROWS,found_rows}, + {?CLIENT_LONG_FLAG, long_flag}, + {?CLIENT_CONNECT_WITH_DB, connect_with_db}, + {?CLIENT_NO_SCHEMA, no_schema}, + {?CLIENT_COMPRESS, compress}, + {?CLIENT_ODBC, odbc}, + {?CLIENT_LOCAL_FILES, local_files}, + {?CLIENT_IGNORE_SPACE, ignore_space}, + {?CLIENT_PROTOCOL_41, protocol_41}, + {?CLIENT_INTERACTIVE, interactive}, + {?CLIENT_SSL, ssl}, + {?CLIENT_IGNORE_SIGPIPE, ignore_sigpipe}, + {?CLIENT_TRANSACTIONS, transactions}, + {?CLIENT_SECURE_CONNECTION, secure_connection}, + {?CLIENT_MULTI_STATEMENTS, multi_statements}, + {?CLIENT_MULTI_RESULTS, multi_results}, + {?CLIENT_PS_MULTI_RESULTS, ps_multi_results}, + {?CLIENT_PLUGIN_AUTH, plugin_auth}, + {?CLIENT_CONNECT_ATTRS, connect_attrs}, + {?CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA, auth_lenenc_client_data} + ], + lists:flatmap(fun({I,Tag}) -> + case Flag band I of + 0 -> []; + _ -> [Tag] + end + end, Caps). + +-spec decode(binary()) -> {ok, response(), binary()} | {more, binary()}. + +decode(<<Length:24/little, Id, Bin:Length/binary, Rest/binary>>) -> + {ok, decode0(Length, Id, Bin), Rest}; + +decode(<<Length:24/little, _Id, Bin/binary>>) -> + {more, Length - size(Bin)}; + +decode(<<Bin/binary>>) -> + {more, 4 - size(Bin)}. + +-spec decode0(Length::integer(), Id::binary(), Bin::binary()) -> request(). + +decode0(_, Id, <<?COM_FIELD_LIST:8, Info/binary>>) -> #request{ command=?COM_FIELD_LIST, id=Id, info=my_datatypes:string_nul_to_binary(Info) }; -decode(<<16#ffffff:24/little, Id:8, Command:8, Info/binary>>) -> +decode0(16#ffffff, Id, <<Command:8, Info/binary>>) -> #request{ command=Command, id=Id, info=Info, continue=true }; -decode(<<_Length:24/little, Id:8, Command:8, Info/binary>>) -> +decode0(_, Id, <<Command:8, Info/binary>>) -> #request{ command=Command, id=Id, info=Info, continue=false }. + +-spec bin_to_upper(Lower::binary()) -> Upper::binary(). + +bin_to_upper(Lower) when is_binary(Lower) -> + << <<( + if + X >= $a andalso X =< $z -> X-32; + true -> X end + )/integer>> || <<X>> <= Lower >>. diff --git a/src/my_protocol.erl b/src/my_protocol.erl new file mode 100644 index 0000000..e52c5c8 --- /dev/null +++ b/src/my_protocol.erl @@ -0,0 +1,206 @@ +% @doc +% This is a server-side protocol implementation +% +% When server process receives socket, it MUST first call hello: +% +% handle_info({socket, Socket}, State) -> +% {ok, Hello, My} = my_protocol:hello(ConnectionId), % here ConnectionId is a thread id +% ok = gen_tcp:send(Socket, Hello), +% inet:setopts(Socket, [{active,once}]), +% {noreply, State#state{my = My}}; +% +-module(my_protocol). +-author('Max Lapshin <max@maxidoors.ru>'). +-author('Manuel Rubio <manuel@altenwald.com>'). + +-include("myproto.hrl"). + +-export([init/0, init/1]). +-export([decode/2, decode/1, buffer_bytes/2]). +-export([send_or_reply/2, hello/2, ok/1, error/2, error/3]). +-export([next_packet/1]). + +-export_type([my/0]). + +-record(my, { + connection_id :: non_neg_integer(), %% connection id + hash ::binary(), %% hash for auth + state :: auth | normal, + parse_query = true :: boolean(), %% parse query or not + buffer :: undefined | binary(), + query_buffer = <<>> :: binary(), %% buffer for long queries + socket :: undefined | inet:socket(), %% When socket is set, client will send data + id = 1 :: non_neg_integer() +}). + +-type my() :: #my{}. + +-spec init() -> my(). + +init() -> + init([]). + +-type options() :: proplists:proplist(). + +-spec init(options()) -> my(). + +init(Options) -> + Socket = proplists:get_value(socket, Options), + ParseQuery = proplists:get_value(parse_query, Options, true), + #my{socket = Socket, parse_query = ParseQuery}. + +-spec hello(ConnectionId::non_neg_integer(), my()) -> + {ok, Bin::iodata(), State::my()} | {ok, my()}. + +hello(ConnectionId, #my{} = My) when is_integer(ConnectionId) -> + Hash = list_to_binary( + lists:map(fun + (0) -> 1; + (X) -> X + end, binary_to_list(crypto:rand_bytes(20)))), + Hello = #response{ + id=ConnectionId, + status=?STATUS_HELLO, + info=Hash}, + send_or_reply(Hello, + My#my{connection_id=ConnectionId, hash=Hash, state=auth, id=2}). + +-spec send_or_reply(ok | response(), my()) -> + {ok, Reply::binary(), my()} | {ok, my()}. + +send_or_reply(ok, #my{} = My) -> + ok(My); + +send_or_reply(#response{id = 0} = Response, #my{id = Id} = My) -> + send_or_reply(Response#response{id = Id}, My#my{id = Id+1}); + +send_or_reply(#response{} = Response, #my{socket = Socket} = My) -> + ok = gen_tcp:send(Socket, my_packet:encode(Response)), + {ok, My}. + +-spec ok(my()) -> {ok, Reply::binary(), my()} | {ok, my()}. + +ok(#my{} = My) -> + Response = #response{ + status = ?STATUS_OK, + status_flags = ?SERVER_STATUS_AUTOCOMMIT + }, + send_or_reply(Response, My). + +-spec error(Reason::binary(), my()) -> {ok, Reply::binary(), my()} | {ok, my()}. + +error(Reason, #my{} = My) when is_binary(Reason) -> + error(1045, Reason, My). + +-spec error(Code::non_neg_integer(), Reason::binary(), my()) -> + {ok, Reply::binary(), my()} | {ok, my()}. + +error(Code, Reason, #my{} = My) when is_integer(Code), is_binary(Reason) -> + Response = #response{ + status = ?STATUS_ERR, + error_code = Code, + info = Reason + }, + send_or_reply(Response, My). + +-spec decode(binary(), my()) -> {ok, Reply::request(), my()}. + +decode(Bin, #my{} = My) -> + My1 = buffer_bytes(Bin, My), + decode(My1). + +-spec buffer_bytes(binary(), my()) -> my(). + +buffer_bytes(Bin, #my{buffer = Buffer} = My) when size(Buffer) > 0 andalso size(Bin) > 0 -> + My#my{buffer = <<Buffer/binary, Bin/binary>>}; + +buffer_bytes(Bin, #my{} = My) -> + My#my{buffer = Bin}. + +-spec decode(my()) -> {ok, Reply::request(), my()} | {more, my()}. + +decode(#my{buffer = Bin, state = auth, hash = Hash} = My) when size(Bin) > 4 -> + case my_packet:decode_auth(Bin) of + {more, _} -> + {more, My}; + {ok, #request{info = #user{} = User} = Req, Rest} -> + {ok, + Req#request{info = User#user{server_hash = Hash}}, + My#my{state = normal, buffer = Rest}} + end; + +decode(#my{buffer = Bin, state = normal, parse_query = ParseQuery, + query_buffer = QB} = My) when size(Bin) > 4 -> + case my_packet:decode(Bin) of + {more, _} -> + {more, My}; + {ok, #request{continue = true, info = Info}, Rest} -> + QB1 = case QB of + <<>> -> Info; + _ -> <<QB/binary, Info/binary>> + end, + decode(My#my{buffer = Rest, query_buffer = QB1}); + {ok, #request{continue=false, info=Info, command=CommandCode, id=Id}=Packet, Rest} -> + Query = case QB of + <<>> -> Info; + _ -> <<QB/binary, Info/binary>> + end, + Packet1 = case command_code(CommandCode) of + 'query' when ParseQuery -> + S = size(Query) - 1, + Query2 = case Query of + <<Query1:S/binary,0>> -> Query1; + _ -> Query + end, + SQL = case mysql_proto:parse(Query2) of + {fail,Expected} -> {parse_error, {fail,Expected}, Info}; + Parsed -> Parsed + end, + Packet#request{command = 'query', info = SQL, text = Query2}; + Command -> + Packet#request{command = Command, info = Query} + end, + {ok, Packet1, My#my{buffer = Rest, id = Id + 1}} + end; + +decode(#my{} = My) -> + {more, My}. + +-spec command_code(CommandCode::integer()) -> atom(). + +command_code(?COM_SLEEP) -> sleep; +command_code(?COM_QUIT) -> quit; +command_code(?COM_INIT_DB) -> init_db; +command_code(?COM_QUERY) -> 'query'; +command_code(?COM_FIELD_LIST) -> field_list; +command_code(?COM_CREATE_DB) -> create_db; +command_code(?COM_DROP_DB) -> drop_db; +command_code(?COM_REFRESH) -> refresh; +command_code(?COM_SHUTDOWN) -> shutdown; +command_code(?COM_STATISTICS) -> statistics; +command_code(?COM_PROCESS_INFO) -> process_info; +command_code(?COM_CONNECT) -> connect; +command_code(?COM_PROCESS_KILL) -> process_kill; +command_code(?COM_DEBUG) -> debug; +command_code(?COM_PING) -> ping; +command_code(Else) -> Else. + +-spec next_packet(my()) -> Error::atom() | {ok, Reply::binary(), my()}. + +next_packet(#my{buffer = Buffer, socket = Socket} = My) + when Socket =/= undefined andalso + (Buffer == undefined orelse Buffer == <<>>) -> + case gen_tcp:recv(Socket, 4) of + {ok, <<Length:24/unsigned-little, _>> = Header} -> + case gen_tcp:recv(Socket, Length) of + {ok, Bin} when size(Bin) == Length -> + case decode(<<Header/binary, Bin/binary>>, My) of + {more, My1} -> next_packet(My1); + {ok, Response, My1} -> {ok, Response, My1} + end; + {error, _} = Error -> + Error + end; + {error, _} = Error -> + Error + end. diff --git a/src/my_ranch_worker.erl b/src/my_ranch_worker.erl new file mode 100644 index 0000000..559e6a6 --- /dev/null +++ b/src/my_ranch_worker.erl @@ -0,0 +1,400 @@ +-module(my_ranch_worker). +-author('Max Lapshin <max@maxidoors.ru>'). + +-export([start_server/4, stop_server/1, start_link/4, init_server/4]). + +-export([handle_call/3, handle_info/2, terminate/2]). + +-include("myproto.hrl"). + +-record(server, { + handler :: module(), + state :: any(), + socket :: gen_tcp:socket(), + my :: my_protocol:my() +}). + +start_server(Port, Name, Handler, Args) -> + application:start(ranch), + ranch:start_listener(Name, 10, ranch_tcp, [ + {port, Port}, {backlog,4096}, {max_connections,32768} + ], ?MODULE, [Handler, Args]). + +stop_server(Name) -> + ranch:stop_listener(Name). + + +start_link(ListenerPid, Socket, _Transport, [Handler, Args]) -> + proc_lib:start_link(?MODULE, init_server, + [ListenerPid, Socket, Handler, Args]). + +init_server(ListenerPid, Socket, Handler, _Args) -> + proc_lib:init_ack({ok, self()}), + ranch:accept_ack(ListenerPid), + + My0 = my_protocol:init([{socket, Socket},{parse_query,true}]), + {ok, My1} = my_protocol:hello(42, My0), + case my_protocol:next_packet(My1) of + {ok, #request{info = #user{password = Password} = User}, My2} -> + case Handler:check_pass(User) of + {ok, Password, HandlerState} -> + {ok, My3} = my_protocol:ok(My2), + inet:setopts(Socket, [{active,once}]), + State = #server{ + handler = Handler, + state = HandlerState, + socket = Socket, + my = My3 + }, + gen_server:enter_loop(?MODULE, [], State); + {error, Reason} -> + my_protocol:error(Reason, My2); + {error, Code, Reason} -> + my_protocol:error(Code, Reason, My2) + end; + {error, StartError} -> + {stop, StartError} + end. + + +handle_call(Call, _From, #server{} = Server) -> + {stop, {unknown_call,Call}, Server}. + + +handle_info({tcp, Socket, Bin}, #server{my = My} = Server) -> + My1 = my_protocol:buffer_bytes(Bin, My), + inet:setopts(Socket, [{active, once}]), + try + handle_packets(Server#server{my = My1}) + catch + Class:Error -> + lager:info("~p:~p during handling mysql:\n~p\n", + [Class, Error, erlang:get_stacktrace()]), + {stop, normal, Server} + end; + +handle_info({tcp_closed, _Socket}, + #server{handler=Handler, state=HandlerState}=Server) -> + Handler:terminate(tcp_closed, HandlerState), + {stop, normal, Server}; + +handle_info({tcp_error, _, Error}, + #server{handler = Handler, state = HandlerState} = Server) -> + Handler:terminate({tcp_error, Error}, HandlerState), + {stop, normal, Server}. + + +terminate(_,_) -> + ok. + + +handle_packets(#server{my=My, handler=Handler, state=HandlerState}=Server) -> + case my_protocol:decode(My) of + {ok, Query, My1} -> + try Handler:execute(Query, HandlerState) of + {noreply, HandlerState1} -> + handle_packets(Server#server{my = My1, state = HandlerState1}); + {reply, default, HandlerState1} when Query#request.command == quit -> + Handler:terminate(quit, HandlerState1), + {stop, normal, Server#server{my = My1, state = HandlerState1}}; + {reply, default, HandlerState1} -> + {reply, Reply, HandlerState2} = + default_reply(Query, Handler, HandlerState1), + {ok, My2} = my_protocol:send_or_reply(Reply, My1), + handle_packets(Server#server{my = My2, state = HandlerState2}); + {reply, Response, HandlerState1} -> + {ok, My2} = my_protocol:send_or_reply(Response, My1), + handle_packets(Server#server{my = My2, state = HandlerState1}); + {stop, Reason, HandlerState1} -> + Handler:terminate(Reason, HandlerState1), + {stop, Reason, Server#server{my = My1, state = HandlerState1}} + catch + C:E -> + ST = erlang:get_stacktrace(), + lager:info("~p:~p in MySQL server handler\n" + " * Last input: ~p\n" + " * Stacktrace: ~p\n" + " * State: ~p", [C, E, Query, ST, HandlerState]), + {stop, E, Server} + end; + {more, My1} -> + {noreply, Server#server{my = My1}} + end. + + +default_reply(#request{info=#select{params=[#variable{name = + <<"version_comment">>}]}}, Handler, State) -> + {Version, State1} = case Handler:metadata(version, State) of + {reply, Vsn, State1_} -> {iolist_to_binary(Vsn), State1_}; + {noreply, State1_} -> {<<"erlang myproto server">>, State1_} + end, + Info = { + [#column{name = <<"@@version_comment">>, type=?TYPE_VARCHAR, length=20}], + [[Version]] + }, + {reply, #response{status=?STATUS_OK, info=Info}, State1}; + +default_reply(#request{info=#select{params=[#variable{name = + <<"global.max_allowed_packet">>}]}}, _Handler, State) -> + Info = { + [#column{name = <<"@@global.max_allowed_packet">>, type=?TYPE_LONG, length=20}], + [[4194304]] + }, + {reply, #response{status=?STATUS_OK, info=Info}, State}; + +default_reply(#request{info = #select{params=[#variable{name = + <<"tx_isolation">>, scope = local}]}}, _Handler, State) -> + Info = { + [#column{name = <<"@@tx_isolation">>, type=?TYPE_VARCHAR}], + [[<<"REPEATABLE-READ">>]] + }, + {reply, #response{status=?STATUS_OK, info=Info}, State}; + +default_reply(#request{info = {use,Database}}, Handler, State) -> + {noreply, State1} = Handler:metadata({connect_db,Database}, State), + {reply, #response { + status=?STATUS_OK, info = <<"Changed to ", Database/binary>>, + status_flags = 2 + }, State1}; + +default_reply(#request{command = init_db, info = Database}, Handler, State) -> + {noreply, State1} = Handler:metadata({connect_db,Database}, State), + {reply, #response { + status=?STATUS_OK, info = <<"Changed to ", Database/binary>> + }, State1}; + +default_reply(#request{info = #show{type = databases}}, Handler, State) -> + {Databases, State1} = case Handler:metadata(databases, State) of + {reply, DB, State1_} -> {DB, State1_}; + {noreply, State1_} -> {[<<"myproto">>], State1_} + end, + ResponseFields = {[#column{ + name = <<"Database">>, type=?TYPE_VAR_STRING, length=20, + schema = <<"information_schema">>, table = <<"SCHEMATA">>, + org_table = <<"SCHEMATA">>, org_name = <<"SCHEMA_NAME">> + }], [ [DB] || DB <- Databases]}, + Response = #response{status=?STATUS_OK, info = ResponseFields}, + {reply, Response, State1}; + +default_reply(#request{info = #show{type = collation}}, _Handler, State) -> + ResponseFields = {[ + #column{name = <<"Collation">>, type=?TYPE_VAR_STRING, length=20}, + #column{name = <<"Charset">>, type=?TYPE_VAR_STRING, length=20}, + #column{name = <<"Id">>, type=?TYPE_LONG}, + #column{name = <<"Default">>, type=?TYPE_VAR_STRING, length=20}, + #column{name = <<"Compiled">>, type=?TYPE_VAR_STRING, length=20}, + #column{name = <<"Sortlen">>, type=?TYPE_LONG} + ], + [ + [<<"utf8_bin">>,<<"utf8">>,83,<<"">>,<<"Yes">>,1] + ]}, + {reply, #response{status=?STATUS_OK, info = ResponseFields}, State}; + +default_reply(#request{info = #show{type = variables}}, Handler, State) -> + {Version, State1} = case Handler:metadata(version, State) of + {reply, Vsn, State1_} -> {iolist_to_binary(Vsn), State1_}; + {noreply, State1_} -> {<<"5.6.0">>, State1_} + end, + {Mega, Sec, Micro} = os:timestamp(), + Timestamp = iolist_to_binary( + io_lib:format("~B.~6..0B", [Mega*1000000 + Sec, Micro])), + + DefaultStorageEngine = application:get_env(myproto, default_storage_engine), + Variables = [ + {<<"sql_mode">>, <<"NO_ENGINE_SUBSTITUTION">>}, + {<<"auto_increment_increment">>, <<"1">>}, + {<<"character_set_client">>, <<"utf8">>}, + {<<"character_set_connection">>, <<"utf8">>}, + {<<"character_set_database">>, <<"utf8">>}, + {<<"character_set_results">>, <<"utf8">>}, + {<<"character_set_server">>, <<"utf8">>}, + {<<"character_set_system">>, <<"utf8">>}, + {<<"date_format">>, <<"%Y-%m-%d">>}, + {<<"datetime_format">>, <<"%Y-%m-%d %H:%i:%s">>}, + {<<"default_storage_engine">>, DefaultStorageEngine}, + {<<"timestamp">>, Timestamp}, + {<<"version">>, Version} + ], + ResponseFields = {[ + #column{ + name = <<"Variable_name">>, type=?TYPE_VAR_STRING, length=20, + schema = <<"information_schema">>, table = <<"SCHEMATA">>, + org_table = <<"SCHEMATA">>, org_name = <<"SCHEMA_NAME">> + }, + #column{ + name = <<"Value">>, type=?TYPE_VAR_STRING, length=20, + schema = <<"information_schema">>, table = <<"SCHEMATA">>, + org_table = <<"SCHEMATA">>, org_name = <<"SCHEMA_NAME">> + } + ], [tuple_to_list(V) || V <- Variables]}, + {reply, #response{status=?STATUS_OK, info = ResponseFields}, State1}; + +default_reply(#request{info = #select{params = + [#function{name = <<"DATABASE">>}]}}, _Handler, State) -> + ResponseFields = {[ + #column{ + name = <<"DATABASE()">>, type=?TYPE_VAR_STRING, length=102, + flags = 0, decimals = 31 + } + ], []}, + Response = #response{status=?STATUS_OK, info = ResponseFields}, + {reply, Response, State}; + +default_reply(#request{info = #show{type = tables}}, Handler, State) -> + {DB, Tables, State1} = case Handler:metadata(tables, State) of + {reply, {DB_, Tables_}, State1_} -> {DB_, Tables_, State1_}; + {noreply, State1_} -> {<<"myproto">>, [], State1_} + end, + ResponseFields = {[ + #column{ + name = <<"Tables_in_", DB/binary>>, type=?TYPE_VAR_STRING, + schema = DB, table = <<"TABLE_NAMES">>, + org_table = <<"TABLE_NAMES">>, org_name = <<"TABLE_NAME">>, + flags = 256, length = 192, decimals = 0 + } + ], [ [Table] || Table <- Tables] }, + Response = #response{status=?STATUS_OK, info = ResponseFields}, + {reply, Response, State1}; + +default_reply(#request{info = #describe{table = #table{name = Table}}}, Handler, State) -> + default_reply(#request{info = #show{type = fields, from = Table, full = false }}, Handler, State); + +default_reply(#request{info = #show{type = fields, from = Table, full = Full}}, + Handler, State) -> + {reply,{_DB,Table,Fields},State1} = Handler:metadata({fields,Table},State), + Header = [ + #column{ + name = <<"Field">>, org_name = <<"COLUMN_NAME">>, + type = ?TYPE_VAR_STRING, table = <<"COLUMNS">>, + org_table = <<"COLUMNS">>, schema = <<"information_schema">>, + length = 192, flags = 256 + }, + #column{ + name = <<"Type">>, org_name = <<"COLUMN_TYPE">>, + type = ?TYPE_BLOB, table = <<"COLUMNS">>, org_table = <<"COLUMNS">>, + schema = <<"information_schema">>, length = 589815, flags = 256 + }, + #column{ + name = <<"Null">>, org_name = <<"IS_NULLABLE">>, + type = ?TYPE_VAR_STRING, table = <<"COLUMNS">>, + org_table = <<"COLUMNS">>, schema = <<"information_schema">>, + length = 9, flags = 256 + }, + #column{ + name = <<"Key">>, org_name = <<"COLUMN_KEY">>, + type = ?TYPE_VAR_STRING, table = <<"COLUMNS">>, + org_table = <<"COLUMNS">>, schema = <<"information_schema">>, + length = 9, flags = 256 + }, + #column{ + name = <<"Default">>, org_name = <<"COLUMN_DEFAULT">>, + type = ?TYPE_BLOB, table = <<"COLUMNS">>, org_table = <<"COLUMNS">>, + schema = <<"information_schema">>, length = 589815, flags = 256 + }, + #column{ + name = <<"Extra">>, org_name = <<"EXTRA">>, type = ?TYPE_VAR_STRING, + table = <<"COLUMNS">>, org_table = <<"COLUMNS">>, + schema = <<"information_schema">>, length = 90, flags = 256 + } + ] ++ case Full of + true -> [ + #column{ + name = <<"Collation">>, org_name = <<"COLLATION_NAME">>, + type = ?TYPE_VAR_STRING, table = <<"COLUMNS">>, + org_table = <<"COLUMNS">>, schema = <<"information_schema">>, + length = 96, flags = 256 + }, + #column{ + name = <<"Privileges">>, org_name = <<"PRIVILEGES">>, + type = ?TYPE_VAR_STRING, table = <<"COLUMNS">>, + org_table = <<"COLUMNS">>, schema = <<"information_schema">>, + length = 240, flags = 256 + }, + #column{ + name = <<"Comment">>, org_name = <<"COLUMN_COMMENT">>, + type = ?TYPE_VAR_STRING, table = <<"COLUMNS">>, + org_table = <<"COLUMNS">>, schema = <<"information_schema">>, + length = 3072, flags = 256 + } + ]; + false -> [] + end, + Rows = lists:map(fun({Name,Type}) -> + [ + atom_to_binary(Name,latin1), + case Type of + string -> <<"varchar(255)">>; + boolean -> <<"tinyint(1)">>; + _ -> <<"bigint(20)">> + end, + <<"YES">>, + <<>>, + undefined, + <<>> + ] ++ case Full of + true -> [ + case Type of + string -> <<"utf8_general_ci">>; + _ -> undefined + end, + <<"select,insert,update,references">>, + <<>> + ]; + false -> [] + end + end, Fields), + {reply, #response{status=?STATUS_OK, info = {Header, Rows}}, State1}; + + +default_reply(#request{ + info = #show{type = create_table, from = Table}}, Handler, State) -> + {reply, {_DB, Table, Fields}, State1} = Handler:metadata({fields,Table}, State), + CreateTable = iolist_to_binary([ + "CREATE TABLE `", Table, "` (\n", + tl(lists:flatmap(fun({Name,Type}) -> + [ + ",", + "`", atom_to_binary(Name,latin1), "` ", + case Type of + string -> "varchar(255)"; + integer -> "bigint(20)"; + boolean -> "tinyint(1)" + end, + "\n" + ] + end, Fields)), + ")" + ]), + Response = {[ + #column{ + name = <<"Table">>, type = ?TYPE_VAR_STRING, flags = 256, + decimals = 31, length = 192 + }, + #column{ + name = <<"Create Table">>, type = ?TYPE_VAR_STRING, flags = 256, + decimals = 31, length = 3072 + } + ], [ [Table, CreateTable] ]}, + {reply, #response{status=?STATUS_OK, info = Response}, State1}; + +default_reply(#request{command = field_list, info = Table}, Handler, State) -> + {reply,{DB,Table,Fields},State1} = Handler:metadata({fields,Table}, State), + Reply = [#column{ + schema = DB, table = Table, org_table = Table, + name = to_b(Field), org_name = to_b(Field), length = 20, + type = case Type of + string -> ?TYPE_VAR_STRING; + integer -> ?TYPE_LONGLONG; + boolean -> ?TYPE_TINY + end} || {Field,Type} <- Fields ], + {reply, #response{status=?STATUS_OK, info = {Reply}}, State1}; + +default_reply(#request{command = ping}, _Handler, State) -> + {reply, #response{status = ?STATUS_OK, id = 1}, State}; + +default_reply(_, _Handler, State) -> + {reply, #response{status=?STATUS_OK}, State}. + + +to_b(Atom) when is_atom(Atom) -> atom_to_binary(Atom, latin1); +to_b(Bin) when is_binary(Bin) -> Bin. diff --git a/src/my_request.erl b/src/my_request.erl index 777efce..4d217ae 100644 --- a/src/my_request.erl +++ b/src/my_request.erl @@ -1,11 +1,11 @@ -module(my_request). --author('bombadil@bosqueviejo.net'). +-author('Manuel Rubio <manuel@altenwald.com>'). -behaviour(gen_fsm). -define(SERVER, ?MODULE). --include("../include/myproto.hrl"). +-include("myproto.hrl"). -export([start/4, check_clean_pass/2, check_sha1_pass/2, sha1_hex/1, to_hex/1]). -export([init/1, handle_sync_event/4, handle_event/3, handle_info/3, @@ -39,10 +39,12 @@ start(Socket, Id, Handler, ParseQuery) -> -spec sha1_hex(Data :: binary()) -> binary(). sha1_hex(Data) -> - to_hex(crypto:sha(Data)). + to_hex(crypto:hash(sha, Data)). --spec to_hex(Hash :: binary()) -> binary(). +-spec to_hex(Hash :: binary() | undefined) -> binary(). +to_hex(<<>>) -> + <<"undefined">>; to_hex(undefined) -> <<"undefined">>; to_hex(<<X:160/big-unsigned-integer>>) -> @@ -51,10 +53,10 @@ to_hex(<<X:160/big-unsigned-integer>>) -> -spec check_sha1_pass(Pass::binary(), Salt::binary()) -> binary(). check_sha1_pass(Stage, Salt) -> - Res = crypto:sha_final( - crypto:sha_update( - crypto:sha_update(crypto:sha_init(), Salt), - crypto:sha(Stage) + Res = crypto:hash_final( + crypto:hash_update( + crypto:hash_update(crypto:hash_init(sha), Salt), + crypto:hash(sha, Stage) ) ), crypto:exor(Stage, Res). @@ -62,7 +64,7 @@ check_sha1_pass(Stage, Salt) -> -spec check_clean_pass(Pass::binary(), Salt::binary()) -> binary(). check_clean_pass(Pass, Salt) -> - check_sha1_pass(crypto:sha(Pass), Salt). + check_sha1_pass(crypto:hash(sha, Pass), Salt). %% callbacks @@ -81,12 +83,18 @@ init([Socket, Id, Handler, ParseQuery]) -> info=Hash }, gen_tcp:send(Socket, my_packet:encode(Hello)), - {ok, auth, #state{socket=Socket, id=Id, hash=Hash, handler=Handler, parse_query=ParseQuery}}. - -handle_info({tcp,_Port, Info}, auth, StateData=#state{hash=Hash,socket=Socket,handler=Handler}) -> - #request{info=#user{ + {ok, auth, #state{ + socket=Socket, + id=Id, + hash=Hash, + handler=Handler, + parse_query=ParseQuery}}. + +handle_info({tcp,_Port, Info}, auth, + #state{hash=Hash,socket=Socket,handler=Handler}=StateData) -> + {ok, #request{info=#user{ name=User, password=Password - }} = my_packet:decode_auth(Info), + }}, <<>>} = my_packet:decode_auth(Info), lager:debug("Hash=~p; Pass=~p~n", [to_hex(Hash),to_hex(Password)]), case Handler:check_pass(User, Hash, Password) of {ok, Password, HandlerState} -> @@ -119,18 +127,20 @@ handle_info({tcp,_Port, Info}, auth, StateData=#state{hash=Hash,socket=Socket,ha {stop, normal, StateData} end; -handle_info({tcp,_Port,Msg}, normal, #state{socket=Socket,handler=Handler,packet=Packet,handler_state=HandlerState}=StateData) -> +handle_info({tcp,_Port,Msg}, normal, + #state{socket=Socket,handler=Handler,packet=Packet, + handler_state=HandlerState}=StateData) -> case my_packet:decode(Msg) of - #request{continue=true, info=Info}=Request -> + {ok, #request{continue=true, info=Info}=Request, <<>>} -> lager:debug("Received (partial): ~p~n", [Request]), {next_state, normal, StateData#state{packet = <<Packet/binary, Info/binary>>}}; - #request{continue=false, id=Id, info=Info}=Request -> + {ok, #request{continue=false, id=Id, info=Info, command=Command}=Request, <<>>} -> lager:debug("Received: ~p~n", [Request]), FullPacket = <<Packet/binary, Info/binary>>, - Query = case StateData#state.parse_query of + Query = case StateData#state.parse_query andalso Command =:= ?COM_QUERY of false -> Request#request{info = FullPacket}; true -> - case mysql:parse(FullPacket) of + case mysql_proto:parse(FullPacket) of {fail,Expected} -> lager:error("SQL invalid: ~p~n", [Expected]), Request#request{info = FullPacket}; @@ -141,12 +151,18 @@ handle_info({tcp,_Port,Msg}, normal, #state{socket=Socket,handler=Handler,packet Request#request{info = Parsed} end end, - {Response, NewHandlerState} = Handler:execute(Query, HandlerState), - lager:debug("Response: ~p~n", [Response]), - gen_tcp:send(Socket, my_packet:encode( - Response#response{id = Id+1} - )), - {next_state, normal, StateData#state{packet = <<"">>,handler_state=NewHandlerState}} + NewStateData = StateData#state{packet = <<"">>}, + case Handler:execute(Query, HandlerState) of + {noreply, NewHandlerState} -> + {next_state, normal, NewStateData#state{handler_state=NewHandlerState}}; + {reply, Response, NewHandlerState} -> + gen_tcp:send(Socket, my_packet:encode( + Response#response{id = Id+1} + )), + {next_state, normal, NewStateData#state{handler_state=NewHandlerState}}; + {stop, Reason, NewHandlerState} -> + {stop, Reason, NewStateData#state{handler_state=NewHandlerState}} + end end; handle_info({tcp_closed, _Socket}, _StateName, #state{id=Id}=StateData) -> diff --git a/src/myproto.app.src b/src/myproto.app.src index e781916..a4803df 100644 --- a/src/myproto.app.src +++ b/src/myproto.app.src @@ -1,15 +1,16 @@ {application, myproto, [ - {description, "MySQL Protocol Server"}, - {vsn, git}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {mod, {myproto_app, []}}, - {env, [ - {port, 3306}, - {server_sign, <<"5.5-myproto">>}, - {handler, my_dummy_handler} - ]} + {description, "MySQL Protocol Server"}, + {vsn, git}, + {registered, []}, + {applications, [ + kernel, + stdlib + ]}, + {mod, {myproto, []}}, + {env, [ + {port, 3306}, + {server_sign, <<"5.5-myproto">>}, + {default_storage_engine, <<"myproto">>}, + {handler, my_dummy_handler} + ]} ]}. diff --git a/src/myproto.erl b/src/myproto.erl new file mode 100644 index 0000000..ca3ca60 --- /dev/null +++ b/src/myproto.erl @@ -0,0 +1,48 @@ +-module(myproto). +-author('Manuel Rubio <manuel@altenwald.com>'). + +-behaviour(application). +-behaviour(supervisor). + +%% Application callbacks +-export([start/2, stop/1]). + +%% Supervisor callbacks +-export([init/1]). + +%% Helper macro for declaring children of supervisor +-define(CHILD(I, J), {I, {I, start_link, J}, permanent, 5000, worker, [I]}). + +%% Easy start command +-export([start/0]). + +start() -> + application:start(myproto). + +%% =================================================================== +%% Application callbacks +%% =================================================================== + +start(_StartType, _StartArgs) -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +stop(_State) -> + ok. + +%% =================================================================== +%% Supervisor callbacks +%% =================================================================== + +init([]) -> + Acceptor = case application:get_env(myproto, port) of + {ok, Port} -> + {ok, Handler} = application:get_env(myproto, handler), + ParseQuery = case application:get_env(myproto, parse_query) of + {ok, PQ} -> PQ; + _ -> false + end, + [?CHILD(my_acceptor, [Port, Handler, ParseQuery])]; + undefined -> + [] + end, + {ok, { {one_for_one, 5, 10}, Acceptor} }. diff --git a/src/myproto_app.erl b/src/myproto_app.erl deleted file mode 100644 index 6fb5fc1..0000000 --- a/src/myproto_app.erl +++ /dev/null @@ -1,22 +0,0 @@ --module(myproto_app). - --behaviour(application). - -%% Application callbacks --export([start/2, stop/1]). - -%% =================================================================== -%% Application callbacks -%% =================================================================== - -start(_StartType, _StartArgs) -> - {ok, Port} = application:get_env(myproto, port), - {ok, Handler} = application:get_env(myproto, handler), - ParseQuery = case application:get_env(myproto, parse_query) of - {ok, PQ} -> PQ; - _ -> false - end, - myproto_sup:start_link(Port, Handler, ParseQuery). - -stop(_State) -> - ok. diff --git a/src/myproto_sup.erl b/src/myproto_sup.erl deleted file mode 100644 index bfd8ff3..0000000 --- a/src/myproto_sup.erl +++ /dev/null @@ -1,33 +0,0 @@ - --module(myproto_sup). - --behaviour(supervisor). - -%% API --export([start_link/3]). - -%% Supervisor callbacks --export([init/1]). - -%% Helper macro for declaring children of supervisor --define(CHILD(I, J), {I, {I, start_link, J}, permanent, 5000, worker, [I]}). - -%% =================================================================== -%% API functions -%% =================================================================== - --spec start_link(Port::integer(), Handler::atom(), ParseQuery::boolean()) -> - {ok, pid()} | {error, Reason::term()}. - -start_link(Port, Handler, ParseQuery) -> - supervisor:start_link({local, ?MODULE}, ?MODULE, [Port, Handler, ParseQuery]). - -%% =================================================================== -%% Supervisor callbacks -%% =================================================================== - -init([Port, Handler, Parser]) -> - {ok, { {one_for_one, 5, 10}, [ - ?CHILD(my_acceptor, [Port, Handler, Parser]) - ]} }. - diff --git a/src/mysql.peg b/src/mysql.peg deleted file mode 100644 index 14b3456..0000000 --- a/src/mysql.peg +++ /dev/null @@ -1,334 +0,0 @@ -sql <- select_query / update_query / insert_query / delete_query / show_query / desc_query ~; - -% --------------- DESC - -desc_query <- space? describe space table space? ` - [_,_Desc,_,Table,_] = Node, - {describe, Table} -`; - -% --------------- SHOW - -show_query <- show_tables_from / show_tables / show_databases ~; - -show_tables <- space? show (space full)? space (tables_keyword / schemas) space? ` - case Node of - [_,show,[],_,_Tables,_] -> - #show{type=tables, full=false, from=undefined}; - [_,show,[_,full],_,_Tables,_] -> - #show{type=tables, full=true, from=undefined} - end -`; - -show_tables_from <- show_tables space from space key space? ` - [ShowTables,_,from,_,Key,_] = Node, - ShowTables#show{from=Key} -`; - -show_databases <- space? show space databases space? ` - #show{type=databases} -`; - -% --------------- SELECT - -select_query <- select_limit / select_order / select_group / select_where / select_from / select_simple ~; - -select_simple <- space? select space params space? ` - #select{params=lists:nth(4, Node)} -`; - -select_from <- select_simple space from space tables space? ` - [#select{params=Query}, _, _From, _, Tables, _] = Node, - #select{params=Query, tables=Tables} -`; - -select_where <- select_from space where space conditions space? ` - [Select, _, _Where, _, Conditions, _] = Node, - Select#select{conditions=Conditions} -`; -select_group <- ( select_where / select_from ) space group_by space group_datas space? ` - [Select, _, _GroupBy, _, Groups, _] = Node, - Select#select{group=Groups} -`; -select_order <- ( select_group / select_where / select_from ) space order_by space order_datas space? ` - [Select, _, _OrderBy, _, Orders, _] = Node, - Select#select{order=Orders} -`; - -select_limit <- ( select_order / select_group / select_where / select_from / select_simple ) space limit space integer (space offset space integer)? space? ` - case Node of - [Select,_,limit,_,Limit,[],_] -> - Select#select{limit=Limit}; - [Select,_,limit,_,Limit,[_,offset,_,Offset],_] -> - Select#select{limit=Limit, offset=Offset} - end -`; - -order_datas <- head:order_data tail:( space? ',' space? order_data )* ` - [proplists:get_value(head, Node)|[ lists:nth(4,I) || I <- proplists:get_value(tail, Node) ]] -`; -order_data <- (key / integer) (space sort)? ` - case Node of - [Key, [_, Sort]] -> #order{key=Key, sort=Sort}; - [Key, []] -> #order{key=Key, sort=asc} - end -`; -sort <- asc / desc ~; - -group_datas <- head:group_data tail:( space? ',' space? group_data )* ` - [proplists:get_value(head, Node)|[ lists:nth(4,I) || I <- proplists:get_value(tail, Node) ]] -`; -group_data <- key / integer ~; - - -% ---------------- UPDATE - -update_query <- update_where / update_simple ~; -update_simple <- space? update space table_general space set space sets space? ` - #update{table=lists:nth(4, Node), set=lists:nth(8, Node)} -`; -update_where <- update_simple space where space conditions space? ` - [Update, _, _Where, _, Conditions, _] = Node, - Update#update{conditions=Conditions} -`; -sets <- head:set_value tail:( space? ',' space? set_value )* ` - [proplists:get_value(head, Node)| [ lists:nth(4, I) || I <- proplists:get_value(tail, Node) ] ] -`; -set_value <- key space? '=' space? param_general ` - #set{key=lists:nth(1, Node), value=lists:nth(5, Node)} -`; - -% ---------------- DELETE - -delete_query <- delete_where / delete_simple ~; -delete_simple <- space? delete space table_general space? ` - #delete{table=lists:nth(4, Node)} -`; -delete_where <- delete_simple space where space conditions space? ` - [Delete, _, _Where, _, Conditions, _] = Node, - Delete#delete{conditions=Conditions} -`; - -% ---------------- INSERT - -insert_query <- insert_values_keys / insert_values / insert_set ~; -insert_values_keys <- space? insert space table_general space? '(' space? keys space? ')' space values space? '(' space? params space? ')' space? ` - Values = lists:zipwith(fun(X,Y) -> - #set{key=X, value=Y} - end, lists:nth(8, Node), lists:nth(16, Node)), - #insert{table=lists:nth(4, Node), values=Values} -`; -insert_values <- space? insert space table_general space values space? '(' space? params space? ')' space? ` - #insert{table=lists:nth(4, Node), values=lists:nth(10, Node)} -`; -insert_set <- space? insert space table_general space set space sets space? ` - #insert{table=lists:nth(4, Node), values=lists:nth(8, Node)} -`; - -% ---------------- COMMON TYPES - -tables <- head:table tail:( space? ',' space? table )* ` - [proplists:get_value(head, Node)|[ lists:nth(4,I) || I <- proplists:get_value(tail, Node) ]] -`; - -table_general <- table_alias / table_value ~; -table <- table_alias / table_value / param_sql ~; -table_alias <- key space as space key ` - #table{name=lists:nth(1, Node), alias=lists:nth(5, Node)} -`; -table_value <- key ` - #table{name=Node, alias=Node} -`; - -comparator <- '<=' / '=<' / '=>' / '>=' / '<>' / '!=' / '<' / '>' / '=' ` -case Node of - <<"<=">> -> lte; - <<"=<">> -> lte; - <<">=">> -> gte; - <<"=>">> -> gte; - <<"!=">> -> neq; - <<"<>">> -> neq; - <<"<">> -> lt; - <<">">> -> gt; - <<"=">> -> eq -end -`; - -conditions <- conditions_normal_chain / conditions_normal / conditions_parens_chain / conditions_parens ~; -conditions_parens_chain <- space? '(' conditions ')' space? nexo space conditions space? ` - case Node of - [_,_,Cond,_,_,Nexo,_,Next,_] -> #condition{nexo=Nexo, op1=Cond, op2=Next} - end -`; -conditions_parens <- space? '(' first:conditions ')' space? ` - proplists:get_value(first, Node) -`; -conditions_normal_chain <- space? condition space nexo space conditions space? ` - case Node of - [_,Cond,_,Nexo,_,Next,_] -> #condition{nexo=Nexo, op1=Cond, op2=Next} - end -`; -conditions_normal <- space? condition space? `lists:nth(2, Node)`; -condition <- condition_comp / condition_set ~; -condition_set <- param space? set_comp subquery ` - #condition{nexo=lists:nth(3,Node), op1=lists:nth(1,Node), op2=lists:nth(4,Node)} -`; -condition_comp <- param space? comparator space? param ` - #condition{nexo=lists:nth(3,Node), op1=lists:nth(1,Node), op2=lists:nth(5,Node)} -`; - -subquery <- space? '(' space? ( select_query / set_datas ) space? ')' space? ` - #subquery{subquery=lists:nth(4, Node)} -`; -set_datas <- head:set_data tail:( space? ',' space? set_data )* ` - [proplists:get_value(head, Node)|[ lists:nth(4,I) || I <- proplists:get_value(tail, Node) ]] -`; -set_data <- value ~; - -nexo <- nexo_and / nexo_or ~; -set_comp <- in / exist / all / any ~; - -params <- head:param tail:( space? ',' space? param )* ` - [proplists:get_value(head, Node)|[ lists:nth(4,I) || I <- proplists:get_value(tail, Node) ]] -`; - -param_general <- param_var / param_function / param_value / param_key / param_sql ~; -param <- param_var / param_arithmetic / param_function / param_value / param_all / param_all_alias / param_key_alias / param_key / param_sql ~; -param_sql <- subquery (space as space key)? ` - case Node of - [#subquery{subquery=Query}, [_,_As,_,Key]] -> #subquery{name=Key, subquery=Query}; - [#subquery{subquery=Query}, []] -> #subquery{subquery=Query} - end -`; -param_key_alias <- key '\.' key (space as space key)? ` - case Node of - [Alias, _, Val, [_, _As, _, Key]] -> #key{alias=Key, name=Val, table=Alias}; - [Alias, _, Val, []] -> #key{alias=Val, name=Val, table=Alias} - end -`; -param_key <- key (space as space key)? ` - case Node of - [Val, [_, _As, _, Key]] -> #key{alias=Key, name=Val}; - [Val, []] -> #key{alias=Val, name=Val} - end -`; -param_value <- value (space as space key)? ` - case Node of - [Val, [_, _As, _, Key]] -> #value{name=Key, value=Val}; - [Val, []] -> #value{value=Val} - end -`; -param_var <- var (space as space key)? ` - case Node of - [Var, [_, _As, _, Key]] -> Var#variable{label=Key}; - [Var, []] -> Var - end -`; -param_all <- '*' `#all{}`; -param_all_alias <- key '\.' '*' `#all{table=lists:nth(1,Node)}`; -param_function <- key space? '(' space? params? space? ')' (space as space key)? ` - case Node of - [Name, _, _, _, Params, _, _, [_, _As, _, Key]] -> - #function{name=Name, params=Params, alias=Key}; - [Name, _, _, _, Params, _, _, []] -> - #function{name=Name, params=Params} - end -`; - -% --------------- From arithmetic (extras in neotoma) - -param_wa <- param_function / param_value / param_all / param_all_alias / param_key_alias / param_key / param_sql ~; -param_arithmetic <- additive ~; -additive <- multitive space? ( "+" / "-" ) space? additive / mul:multitive ` - case Node of - [A, _, Type, _, B] -> #operation{type=Type,op1=A,op2=B}; - {mul,Param} -> Param - end -`; -multitive <- primary space? ( "*" / "/" ) space? Mul:multitive / pri:primary ` - case Node of - [A, _, Type, _, {'Mul', B}] -> #operation{type=Type, op1=A, op2=B}; - {pri,Param} -> Param - end -`; -primary <- par:("(" space? add:additive space? ")") / dec:param_wa ` - case Node of - {dec,Param} -> Param; - {par,List} -> proplists:get_value(add,List) - end -`; - -% ------------- reserved words - -describe <- #(?i)desc# / #(?i)describe# `describe`; -limit <- #(?i)limit# `limit`; -offset <- #(?i)offset# `offset`; -full <- #(?i)full# `full`; -schemas <- #(?i)schemas# `schemas`; -show <- #(?i)show# `show`; -tables_keyword <- #(?i)tables# `tables`; -databases <- #(?i)databases# `databases`; -update <- #(?i)update# `update`; -select <- #(?i)select# `select`; -set <- #(?i)set# `set`; -from <- #(?i)from# `from`; -where <- #(?i)where# `where`; -as <- #(?i)as# `as`; -nexo_or <- #(?i)or# `nexo_or`; -nexo_and <- #(?i)and# `nexo_and`; -in <- #(?i)in# `in`; -any <- #(?i)any# `in`; -exist <- #(?i)exist# `exist`; -all <- #(?i)all# `all`; -group_by <- #(?i)group +by# `group_by`; -asc <- #(?i)asc# `asc`; -desc <- #(?i)desc# `desc`; -order_by <- #(?i)order +by# `order_by`; -delete <- #(?i)delete +from# `delete`; -insert <- #(?i)insert +into# `insert`; -values <- #(?i)values# `values`; - -% complex types -keys <- head:key tail:( space? ',' space? key )* ` - [proplists:get_value(head, Node)|[ lists:nth(4,I) || I <- proplists:get_value(tail, Node) ]] -`; -key <- '`' chars:(!'`' ("\\\\" / '\\`' / .))+ '`' / [a-zA-Z] [A-zA-Z0-9_]* ` - case length(Node) of - 3 -> iolist_to_binary(proplists:get_value(chars, Node)); - 2 -> iolist_to_binary([lists:nth(1,Node)|lists:nth(2,Node)]) - end -`; -value <- string / null / number ~; - -var <- '@' '@'? key ` - [_L,G,Key] = Node, - Scope = case G of - [] -> global; - _ -> local - end, - #variable{name=Key, scope=Scope} -`; - -% Basic types -string <- "'" ((!"'" ("\\\\" / "\\'" / .)) ("''")?)* "'" `binary:replace(iolist_to_binary(lists:nth(2, Node)), <<"''">>, <<"'">>)`; -number <- float / integer ~; -integer <- [0-9]+ ` - list_to_integer(lists:flatten([ binary_to_list(I) || I <- Node ])) -`; -float <- [0-9]* '.' [0-9]+ ` - case Node of - [Int,_,Dec] when Int =/= [] -> - list_to_float( - lists:flatten([ binary_to_list(I) || I <- Int ]) ++ "." ++ - lists:flatten([ binary_to_list(D) || D <- Dec ]) - ); - [_,_,[Dec]] -> - list_to_float("0." ++ lists:flatten([ binary_to_list(D) || D <- Dec ])) - end -`; -space <- [ \t\n\s\r]* ~; -null <- [nN] [uU] [lL] [lL] `null`; - -` --include("../include/sql.hrl"). -` diff --git a/src/mysql_proto.peg b/src/mysql_proto.peg new file mode 100644 index 0000000..d4a4a96 --- /dev/null +++ b/src/mysql_proto.peg @@ -0,0 +1,569 @@ +sql <- set_query / select_query / update_query / insert_query / delete_query / show_query / desc_query / use_query / + transaction_query / account_management_query ~; + +% --------------- USE + +transaction_query <- space? cmd:(begin_cmd / commit / rollback) space? ` + proplists:get_value(cmd,Node) +`; + +use_query <- space? use space database space? ` + [_,_Use,_,Database,_] = Node, + {use, Database} +`; + + +% --------------- DESC + +desc_query <- space? describe space table space? ` + [_,_Desc,_,Table,_] = Node, + #describe{table = Table} +`; + +% --------------- SHOW + +show_query <- show_status / show_create_table / show_tables_from / show_tables_like / show_tables / show_databases / + show_collation_where / show_collation / + show_variables_where / show_variables_like / show_variables / show_fields ~; + +show_status <- space? show space table_keyword space status space like space like:string space? ` + #show{type=status, from=proplists:get_value(like, Node)} +`; + +show_tables <- space? show (space full)? space (tables_keyword / schemas) space? ` + case Node of + [_,show,[],_,_Tables,_] -> + #show{type=tables, full=false, from=undefined}; + [_,show,[_,full],_,_Tables,_] -> + #show{type=tables, full=true, from=undefined} + end +`; + +show_create_table <- space? show space #(?i)create# space table_keyword space key:key space? ` + #show{type = create_table, from=proplists:get_value(key,Node)} +`; + +show_tables_from <- show_tables:show_tables space from space key:key space? ` + ShowTables = proplists:get_value(show_tables, Node), + ShowTables#show{from=proplists:get_value(key,Node)} +`; + +show_tables_like <- show_tables:show_tables space like space pattern:string space? ` + ShowTables = proplists:get_value(show_tables, Node), + ShowTables#show{from={like,proplists:get_value(pattern,Node)}} +`; + +show_databases <- space? show space databases space? ` + #show{type=databases} +`; + +show_collation_where <- space? show space collation space where space conditions:conditions space? ` + #show{type=collation, conditions = proplists:get_value(conditions,Node)} +`; + +show_collation <- space? show space collation space? ` + #show{type=collation} +`; + + +show_variables_where <- space? show space variables space where space conditions:conditions space? ` + #show{type=variables, conditions = proplists:get_value(conditions,Node)} +`; + +show_variables_like <- space? show space variables space like space pattern:string space? ` + #show{type=variables, conditions = {like, proplists:get_value(pattern,Node)}} +`; + +show_variables <- space? show space variables space? ` + #show{type=variables} +`; + + +show_fields <- space? show full:(space full)? space (fields_keyword) space from space key:key space? ` + Full = lists:member(full,proplists:get_value(full,Node)), + #show{type=fields, full=Full, from = proplists:get_value(key,Node)} +`; + + +% --------------- SET + +set_query <- set space head:system_set tail:(space? ',' space? s:system_set )* space? ` + Head = proplists:get_value(head,Node), + Tail = [proplists:get_value(s,N) || N <- proplists:get_value(tail,Node)], + #system_set{'query' = [Head|Tail]} +`; + +system_set <- var:set_var space? '=' space? val:value / names:'NAMES' space val:string / names:'NAMES' space val:charset ` + case proplists:get_value(names,Node) of + undefined -> + {proplists:get_value(var,Node), proplists:get_value(val,Node)}; + N -> + {#variable{name = N, scope = session}, proplists:get_value(val,Node)} + end +`; + +% --------------- SELECT + +select_query <- select_limit / select_order / select_group / select_where / select_from / select_simple ~; + +select_simple <- space? select space params space? ` + #select{params=lists:nth(4, Node)} +`; + +select_from <- select_simple space from space tables space? ` + [#select{params=Query}, _, _From, _, Tables, _] = Node, + #select{params=Query, tables=Tables} +`; + +select_where <- select_from space where space conditions space? ` + [Select, _, _Where, _, Conditions, _] = Node, + Select#select{conditions=Conditions} +`; +select_group <- ( select_where / select_from ) space group_by space group_datas space? ` + [Select, _, _GroupBy, _, Groups, _] = Node, + Select#select{group=Groups} +`; +select_order <- ( select_group / select_where / select_from ) space order_by space order_datas space? ` + [Select, _, _OrderBy, _, Orders, _] = Node, + Select#select{order=Orders} +`; + +select_limit <- ( select_order / select_group / select_where / select_from / select_simple ) space limit space integer (space offset space integer)? space? ` + case Node of + [Select,_,limit,_,Limit,[],_] -> + Select#select{limit=Limit}; + [Select,_,limit,_,Limit,[_,offset,_,Offset],_] -> + Select#select{limit=Limit, offset=Offset} + end +`; + +order_datas <- head:order_data tail:( space? ',' space? order_data )* ` + [proplists:get_value(head, Node)|[ lists:nth(4,I) || I <- proplists:get_value(tail, Node) ]] +`; +order_data <- (key '.' key / key / integer ) (space sort)? ` + case Node of + [[_,<<".">>,Key], [_, Sort]] -> #order{key=Key, sort=Sort}; + [Key, [_, Sort]] -> #order{key=Key, sort=Sort}; + [Key, []] -> #order{key=Key, sort=asc} + end +`; +sort <- asc / desc ~; + +group_datas <- head:group_data tail:( space? ',' space? group_data )* ` + [proplists:get_value(head, Node)|[ lists:nth(4,I) || I <- proplists:get_value(tail, Node) ]] +`; +group_data <- key / integer ~; + + +% ---------------- UPDATE + +update_query <- update_where / update_simple ~; +update_simple <- space? update space table_general space set space sets space? ` + #update{table=lists:nth(4, Node), set=lists:nth(8, Node)} +`; +update_where <- update_simple space where space conditions space? ` + [Update, _, _Where, _, Conditions, _] = Node, + Update#update{conditions=Conditions} +`; +sets <- head:set_value tail:( space? ',' space? set_value )* ` + [proplists:get_value(head, Node)| [ lists:nth(4, I) || I <- proplists:get_value(tail, Node) ] ] +`; +set_value <- key space? '=' space? param_general ` + #set{key=lists:nth(1, Node), value=lists:nth(5, Node)} +`; + +% ---------------- DELETE + +delete_query <- delete_where / delete_simple ~; +delete_simple <- space? delete space from space table_general space? ` + #delete{table=lists:nth(6, Node)} +`; +delete_where <- delete_simple space where space conditions space? ` + [Delete, _, _Where, _, Conditions, _] = Node, + Delete#delete{conditions=Conditions} +`; + +% ---------------- INSERT + +insert_query <- insert_values_keys / insert_values / insert_set ~; +insert_values_keys <- space? insert space into space table_general space? '(' space? keys space? ')' space values space? '(' space? params space? ')' space? ` + Values = lists:zipwith(fun(X,Y) -> + #set{key=X, value=Y} + end, lists:nth(10, Node), lists:nth(18, Node)), + #insert{table=lists:nth(6, Node), values=Values} +`; +insert_values <- space? insert space into space table_general space values space? '(' space? params space? ')' space? ` + #insert{table=lists:nth(6, Node), values=lists:nth(12, Node)} +`; +insert_set <- space? insert space into space table_general space set space sets space? ` + #insert{table=lists:nth(6, Node), values=lists:nth(10, Node)} +`; + +% ---------------- Account Management Statements + +account_management_query <- insert_user / grant_sql / drop_user / rename_sql / revoke_sql / set_password ~; +% +drop_user <- space? drop space user space user_at_host space? ` + #management{action = drop, data = #account{access = lists:nth(6, Node)} } +`; + +insert_user <- space? create_user space user_at_host space identified password? space param space? ` + #value{name = undefined,value = Password} = lists:nth(9, Node), + #management{action = create, data = #account{access = [#value{name = <<"password">>, value = Password}|lists:nth(4, Node)]}} +`; + +grant_sql <- space? grant space permission space? on space priv_level ('.' priv_level space / space) to space user_at_host space? (with grant_options*)? space?` + case lists:nth(14,Node) of + [_|_] -> + #management{action = grant, data = #permission{on = lists:nth(8,Node), account = lists:nth(12,Node), conditions = [lists:nth(4,Node)|lists:nth(2,lists:nth(14,Node))]}}; + _ -> + #management{action = grant, data = #permission{on = lists:nth(8,Node), account = lists:nth(12,Node), conditions = lists:nth(4,Node)}} + end +`; + +rename_sql <- space? rename_user space user_at_host space to space user_at_host space? ` + #management{action = rename, data = [#account{access = lists:nth(4, Node)}|lists:nth(8, Node)]} +`; + +revoke_sql <- space? revoke space permission space on space priv_level ('.' priv_level space / space) from space user_at_host space? ` + #management{action = revoke, data = #permission{on = lists:nth(8,Node), account = lists:nth(12,Node), conditions = lists:nth(4,Node)}} +`; + +set_password <- space? set space password space for space user_at_host space '=' space param space? ` + case lists:nth(12, Node) of + #value{name = undefined,value = Password} -> ok; + #key{name = Password, alias = _} -> ok + end, + #management{action = setpasswd, data = #account{access =[#value{name = <<"password">>, value = Password}|lists:nth(8, Node)]}} +`; + +% ---------------- COMMON TYPES + +tables <- head:table tail:( space? ',' space? table )* ` + [proplists:get_value(head, Node)|[ lists:nth(4,I) || I <- proplists:get_value(tail, Node) ]] +`; + +table_general <- table_alias / table_value ~; +table <- table_alias / table_value / param_sql ~; +table_alias <- key space as space key ` + #table{name=lists:nth(1, Node), alias=lists:nth(5, Node)} +`; +table_value <- key ` + #table{name=Node, alias=Node} +`; + +comparator <- '<=' / '=<' / '=>' / '>=' / '<>' / '!=' / '<' / '>' / '=' / like ` +case Node of + <<"<=">> -> lte; + <<"=<">> -> lte; + <<">=">> -> gte; + <<"=>">> -> gte; + <<"!=">> -> neq; + <<"<>">> -> neq; + <<"<">> -> lt; + <<">">> -> gt; + <<"=">> -> eq; + like -> like +end +`; + +conditions <- conditions_normal_chain / conditions_normal / conditions_parens_chain / conditions_parens ~; +conditions_parens_chain <- space? '(' conditions ')' space? nexo space conditions space? ` + case Node of + [_,_,Cond,_,_,Nexo,_,Next,_] -> #condition{nexo=Nexo, op1=Cond, op2=Next} + end +`; +conditions_parens <- space? '(' first:conditions ')' space? ` + proplists:get_value(first, Node) +`; +conditions_normal_chain <- space? condition space nexo space conditions space? ` + case Node of + [_,Cond,_,Nexo,_,Next,_] -> #condition{nexo=Nexo, op1=Cond, op2=Next} + end +`; +conditions_normal <- space? condition space? `lists:nth(2, Node)`; +condition <- condition_comp / condition_set ~; +condition_set <- param space? set_comp subquery ` + #condition{nexo=lists:nth(3,Node), op1=lists:nth(1,Node), op2=lists:nth(4,Node)} +`; +condition_comp <- param space? comparator space? param ` + #condition{nexo=lists:nth(3,Node), op1=lists:nth(1,Node), op2=lists:nth(5,Node)} +`; + +subquery <- space? '(' space? ( select_query / set_datas ) space? ')' space? ` + #subquery{subquery=lists:nth(4, Node)} +`; +set_datas <- head:set_data tail:( space? ',' space? set_data )* ` + [proplists:get_value(head, Node)|[ lists:nth(4,I) || I <- proplists:get_value(tail, Node) ]] +`; +set_data <- value ~; + +nexo <- nexo_and / nexo_or ~; +set_comp <- not_in / in / exist / all / any ~; + +params <- head:param tail:( space? ',' space? param )* ` + [proplists:get_value(head, Node)|[ lists:nth(4,I) || I <- proplists:get_value(tail, Node) ]] +`; + +param_general <- param_var / param_function / param_value / param_key / param_sql ~; +param <- param_var / param_arithmetic / param_function / param_value / param_all / param_all_alias / param_key_alias / param_key / param_sql ~; + +param_sql <- subquery (space as space key)? ` + case Node of + [#subquery{subquery=Query}, [_,_As,_,Key]] -> #subquery{name=Key, subquery=Query}; + [#subquery{subquery=Query}, []] -> #subquery{subquery=Query} + end +`; +param_key_alias <- key '\.' key (space as space key)? ` + case Node of + [Alias, _, Val, [_, _As, _, Key]] -> #key{alias=Key, name=Val, table=Alias}; + [Alias, _, Val, []] -> #key{alias=Val, name=Val, table=Alias} + end +`; +param_key <- key (space as space key)? ` + case Node of + [Val, [_, _As, _, Key]] -> #key{alias=Key, name=Val}; + [Val, []] -> #key{alias=Val, name=Val} + end +`; +param_value <- value (space as space key)? ` + case Node of + [Val, [_, _As, _, Key]] -> #value{name=Key, value=Val}; + [Val, []] -> #value{value=Val} + end +`; +param_var <- var (space as space key)? ` + case Node of + [Var, [_, _As, _, Key]] -> Var#variable{label=Key}; + [Var, []] -> Var + end +`; +param_all <- '*' `#all{}`; +param_all_alias <- key '\.' '*' `#all{table=lists:nth(1,Node)}`; +param_function <- key space? '(' space? params? space? ')' (space as space key)? ` + case Node of + [Name, _, _, _, Params, _, _, [_, _As, _, Key]] -> + #function{name=Name, params=Params, alias=Key}; + [Name, _, _, _, Params, _, _, []] -> + #function{name=Name, params=Params} + end +`; + +% --------------- GRANT Syntax priv_type https://dev.mysql.com/doc/refman/5.0/en/grant.html +priv_level <- priv_part / priv_all / all_for_all / db_name_all / db_name_table / table ~; + +priv_part <- '.' '*' ` + io:fwrite("Nodeall2 ~p ~n ",[Node]), + #all{} +`; + +priv_all <- '*' ` + #all{} +`; + +all_for_all <- '*' '.' '*' ` + #all{table = #all{}} +`; + +db_name_all <- database '.' '*' ` + [DBName,_,_] = Node, + #value{name = DBName, value = #all{}} +`; + +db_name_table <- database '.' table ` + [DBName,_,TableName] = Node, + %table could also be routine name + #value{name = DBName, value = #table{name = TableName}} +`; + +% --------------- Permissions +permission <- head:perms tail:( space? ',' space? perms )* ` + [proplists:get_value(head, Node)|[ lists:nth(4,I) || I <- proplists:get_value(tail, Node) ]] +`; +perms <- all / all_privileges / alter_routine / alter / create_routine / create_temp_tables / create_user / create_view / +event / file / grant_option / index / lock_tables / process / references / reload / repl_client / repl_slave / +show_dbs / show_view / shutdown / super / trigger / update / usage / insert / create / delete / drop / execute / select / update ~; + +% --------------- GRANT Syntax Account Names http://dev.mysql.com/doc/refman/5.6/en/account-names.html + +user_at_host <- param '@' param ` + [{value,undefined,Username},<<"@">>,{value,undefined,Host}] = Node, + [#value{name = <<"username">>, value = Username}, + #value{name = <<"host">>, value = Host}] +`; + +% --------------- GRANT Options syntax + +grant_options <- space? (max_queries_per_hour / max_updates_per_hour / max_connections_per_hour / max_user_connections) space? ('=' space? integer / integer) ` + case Node of + [_,What,_,[_Operator,_,Value]] -> #value{name = What, value = Value}; + [_,What,_,Value] -> #value{name = What, value = Value} + end +`; + +% --------------- From arithmetic (extras in neotoma) + +param_wa <- param_function / param_value / param_all / param_all_alias / param_key_alias / param_key / param_sql ~; +param_arithmetic <- additive ~; +additive <- multitive space? ( "+" / "-" ) space? additive / mul:multitive ` + case Node of + [A, _, Type, _, B] -> #operation{type=Type,op1=A,op2=B}; + {mul,Param} -> Param + end +`; +multitive <- primary space? ( "*" / "/" ) space? Mul:multitive / pri:primary ` + case Node of + [A, _, Type, _, {'Mul', B}] -> #operation{type=Type, op1=A, op2=B}; + {pri,Param} -> Param + end +`; +primary <- par:("(" space? add:additive space? ")") / dec:param_wa ` + case Node of + {dec,Param} -> Param; + {par,List} -> proplists:get_value(add,List) + end +`; + +% ------------- reserved words + +status <- #(?i)status# `status`; +like <- #(?i)like# `like`; +use <- #(?i)use# `use`; +describe <- #(?i)describe# / #(?i)desc# `describe`; +limit <- #(?i)limit# `limit`; +offset <- #(?i)offset# `offset`; +full <- #(?i)full# `full`; +schemas <- #(?i)schemas# `schemas`; +show <- #(?i)show# `show`; +fields_keyword <- #(?i)fields# `fields`; +tables_keyword <- #(?i)tables# `tables`; +table_keyword <- #(?i)table# `table`; +databases <- #(?i)databases# `databases`; +collation <- #(?i)collation# `collation`; +variables <- #(?i)variables# `variables`; +update <- #(?i)update# `update`; +execute <- #(?i)execute# `execute`; +create <- #(?i)create# `create`; +select <- #(?i)select# `select`; +set <- #(?i)set# `set`; +from <- #(?i)from# `from`; +where <- #(?i)where# `where`; +as <- #(?i)as# `as`; +nexo_or <- #(?i)or# `nexo_or`; +nexo_and <- #(?i)and# `nexo_and`; +not_in <- #(?i)not +in# `not_in`; +in <- #(?i)in# `in`; +any <- #(?i)any# `in`; +exist <- #(?i)exist# `exist`; +all <- #(?i)all# `all`; +all_privileges <- #(?i)all +privileges# `all_privileges`; +group_by <- #(?i)group +by# `group_by`; +asc <- #(?i)asc# `asc`; +desc <- #(?i)desc# `desc`; +order_by <- #(?i)order +by# `order_by`; +delete <- #(?i)delete# `delete`; +insert <- #(?i)insert# `insert`; +into <- #(?i)into# `into`; +values <- #(?i)values# `values`; +begin_cmd <- #(?i)begin# `'begin'`; +commit <- #(?i)commit# `commit`; +rollback <- #(?i)rollback# `rollback`; +create_user <- #(?i)create +user# `create_user`; +create_view <- #(?i)create +view# `create_view`; +rename_user <- #(?i)rename +user# `rename_user`; +identified <- #(?i)identified +by # `identified`; +password <- #(?i)password# `password`; +grant <- #(?i)grant# `grant`; +usage <- #(?i)usage# `usage`; +on <- #(?i)on# `on`; +to <- #(?i)to# `to`; +user <- #(?i)user# `user`; +drop <- #(?i)drop# `drop`; +with <- #(?i)with# `with`; +database <- key ~; +revoke <- #(?i)revoke# `revoke`; +for <- #(?i)for# `for`; +max_queries_per_hour <- #(?i)max_queries_per_hour# `max_queries_per_hour`; +max_updates_per_hour <- #(?i)max_updates_per_hour# `max_updates_per_hour`; +max_connections_per_hour <- #(?i)max_connections_per_hour# `max_connections_per_hour`; +max_user_connections <- #(?i)max_user_connections# `max_user_connections`; +alter <- #(?i)alter# `alter`; +alter_routine <- #(?i)alter +routine# `alter_routine`; +create_routine <- #(?i)create +routine# `create_routine`; +create_temp_tables <- #(?i)create +temporary +tables# `create_temp_tables`; +event <- #(?i)event# `event`; +file <- #(?i)file# `file`; +grant_option <- #(?i)grant +option# `grant_option`; +index <- #(?i)index# `index`; +lock_tables <- #(?i)lock +tables# `lock_tables`; +references <- #(?i)references# `references`; +reload <- #(?i)reload# `reload`; +repl_client <- #(?i)replication +client# `repl_client`; +repl_slave <- #(?i)replication +slave# `repl_slave`; +show_dbs <- #(?i)show +databases# `show_dbs`; +show_view <- #(?i)show +view# `show_view`; +shutdown <- #(?i)shutdown# `shutdown`; +super <- #(?i)super# `super`; +trigger <- #(?i)trigger# `trigger`; +process <- #(?i)process# `process`; + + +% complex types +keys <- head:key tail:( space? ',' space? key )* ` + [proplists:get_value(head, Node)|[ lists:nth(4,I) || I <- proplists:get_value(tail, Node) ]] +`; +key <- '`' chars:(!'`' ("\\\\" / '\\`' / .))+ '`' / [a-zA-Z] [A-zA-Z0-9_]* ` + case length(Node) of + 3 -> iolist_to_binary(proplists:get_value(chars, Node)); + 2 -> iolist_to_binary([lists:nth(1,Node)|lists:nth(2,Node)]) + end +`; +value <- string / null / number ~; + +var <- '@' '@'? key ('.' key)? ` + [L,G,Key1,Key2] = Node, + Key = iolist_to_binary([Key1,Key2]), + Scope = if + L == [] andalso G == [] -> session; + G == [] -> global; + true -> local + end, + #variable{name=Key, scope=Scope} +`; + +set_var <- '@'? '@'? key ('.' key)? ` + [L,G,Key1,Key2] = Node, + Key = iolist_to_binary([Key1,Key2]), + Scope = if + L == [] andalso G == [] -> session; + G == [] -> global; + true -> local + end, + #variable{name=Key, scope=Scope} +`; + + +% Basic types +charset <- "utf8" / "latin1" ~; +string <- "'" ((!"'" ("\\\\" / "\\'" / .)) ("''")?)* "'" `binary:replace(iolist_to_binary(lists:nth(2, Node)), <<"''">>, <<"'">>)`; +number <- float / integer ~; +integer <- [0-9]+ ` + list_to_integer(lists:flatten([ binary_to_list(I) || I <- Node ])) +`; +float <- [0-9]* '.' [0-9]+ ` + case Node of + [Int,_,Dec] when Int =/= [] -> + list_to_float( + lists:flatten([ binary_to_list(I) || I <- Int ]) ++ "." ++ + lists:flatten([ binary_to_list(D) || D <- Dec ]) + ); + [_,_,[Dec]] -> + list_to_float("0." ++ lists:flatten([ binary_to_list(D) || D <- Dec ])) + end +`; +space <- [ \t\n\s\r]* ~; +null <- [nN] [uU] [lL] [lL] `null`; + +` +-include("../include/sql.hrl"). +` diff --git a/src/nanomysql.erl b/src/nanomysql.erl new file mode 100755 index 0000000..23a0182 --- /dev/null +++ b/src/nanomysql.erl @@ -0,0 +1,227 @@ +-module(nanomysql). +-author('Max Lapshin <max@maxidoors.ru>'). + +-export([connect/1, execute/2, command/3]). +-export([select/2]). + +%% @doc +%% connect("mysql://user:password@127.0.0.1/dbname") +%% +connect(URL) -> + {ok, {mysql, AuthInfo, Host, Port, "/"++DBName, Qs}} = http_uri:parse(URL, [{scheme_defaults,[{mysql,3306}]}]), + Query = case Qs of + "?" ++ Qs1 -> [ list_to_tuple(string:tokens(Part,"=")) || Part <- string:tokens(Qs1,"&") ]; + "" -> [] + end, + + [User, Password] = case string:tokens(AuthInfo, ":") of + [U,P] -> [U,P]; + [U] -> [U,""] + end, + + {ok, Sock} = gen_tcp:connect(Host, Port, [binary,{active,false}]), + + {ok, 0, <<_ProtoVersion, Rest1/binary>>} = read_packet(Sock), + [_ServerVersion, <<_ThreadId:32/little, Scramble1:8/binary, 0, _Caps1:16, Charset, _Status:16, _Caps2:16, + AuthLen, _Reserve1:10/binary, Scramble2:13/binary, _Rest2/binary>>] = binary:split(Rest1, <<0>>), + 21 = AuthLen, + <<Scramble:20/binary, _/binary>> = <<Scramble1/binary, Scramble2/binary>>, + + MaxPacket = 16777216, + Auth = case Password of + <<>> -> 0; + "" -> 0; + _ -> + Digest1 = crypto:hash(sha, Password), + SHA = crypto:hash_final(crypto:hash_update( + crypto:hash_update(crypto:hash_init(sha), Scramble), + crypto:hash(sha, Digest1) + )), + [size(SHA), crypto:exor(Digest1, SHA) ] + end, + + % http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse41 + % CLIENT_CONNECT_WITH_DB = 8 + CapFlags = 16#4003F7CF bor 8, + send_packet(Sock, 1, [<<CapFlags:32/little, (MaxPacket-1):32/little, Charset>>, binary:copy(<<0>>, 23), + [User, 0], Auth]), % Need to send DBName,0 after auth, but mysql doesn't do it + + case read_packet(Sock) of + {ok, 2, <<0,_/binary>> = _AuthReply} -> + case proplists:get_value("login", Query) of + "init_db" -> command(init_db, DBName, Sock); + _ -> ok + end, + {ok, Sock}; + {error, _} = Error -> + Error + end. + + +-record(column, { + name, + type, + length +}). + +execute(Query, Sock) -> + command(3, iolist_to_binary(Query), Sock). + +select(Query, Sock) -> + case execute(Query, Sock) of + {error, Error} -> + error(Error); + {ok, {Columns, Rows}} -> + Names = [binary_to_atom(N,latin1) || {N,_} <- Columns], + [maps:from_list(lists:zip(Names,Row)) || Row <- Rows] + end. + + +command(ping, Info, Sock) -> command(14, Info, Sock); +command(show_fields, Info, Sock) -> command(4, Info, Sock); +command(init_db, Info, Sock) -> command(2, iolist_to_binary(Info), Sock); + +command(2 = Cmd, Info, Sock) -> + send_packet(Sock, 0, [Cmd, Info]), + {ok, _, <<0, _/binary>>} = read_packet(Sock), + ok; + +command(Cmd, Info, Sock) when is_integer(Cmd) -> + send_packet(Sock, 0, [Cmd, Info]), + case read_columns(Sock) of + {error, Error} -> + {error, Error}; + ok -> + {ok, {[],[]}}; + {ok, #{}} = InsertInfo -> + InsertInfo; + {_Cols, Columns} -> % response to query + Rows = read_rows(Columns, Sock), + {ok, {[{Field,type_name(Type)} || #column{name = Field, type = Type} <- Columns], Rows}}; + Columns -> + {ok, {[{Field,type_name(Type)} || #column{name = Field, type = Type} <- Columns]}} + end. + + +read_columns(Sock) -> + case read_packet(Sock) of + {ok, _, <<254, _/binary>>} -> + []; + {ok, _, <<0, Packet/binary>>} -> % 0 is STATUS_OK + {AffectedRows, P1} = varint(Packet), + {LastInsertId, P2} = varint(P1), + <<Status:16/little, Warnings:16/little, Rest/binary>> = P2, + {ok, #{affected_rows => AffectedRows, last_insert_id => LastInsertId, status => Status, warnings => Warnings, info => Rest}}; + {ok, _, <<Cols>>} -> + {Cols, read_columns(Sock)}; % number of columns + {ok, _, FieldBin} -> + {_Cat, B1} = lenenc_str(FieldBin), % Catalog + {_Schema, B2} = lenenc_str(B1), % schema + {_Table, B3} = lenenc_str(B2), % table + {_OrgTable, B4} = lenenc_str(B3), % org_table + {Field, B5} = lenenc_str(B4), % column name + {_OrgName, B6} = lenenc_str(B5), % org_name + <<16#0c, _Charset:16/little, Length:32/little, Type:8, Flags:16, _Decimals:8, _/binary>> = B6, + case get(debug) of + true -> + io:format("name= ~p, cat= ~p, schema= ~p, table= ~p, org_table= ~p, org_name= ~p, flags=~p, type=~p,decimals=~p,length=~p\n", [ + Field, _Cat, _Schema, _Table, _OrgTable, _OrgName, Flags,Type,_Decimals,Length + ]); + _ -> + ok + end, + [#column{name = Field, type = Type, length = Length}|read_columns(Sock)]; + {error, Error} -> + {error, Error} + end. + + + + + +type_name(0) -> decimal; +type_name(1) -> tiny; +type_name(2) -> short; +type_name(3) -> long; +type_name(7) -> timestamp; +type_name(8) -> longlong; +type_name(15) -> varchar; +type_name(16#fc) -> blob; +type_name(16#fd) -> varstring; +type_name(16#fe) -> string; +type_name(T) -> T. + + + + +read_rows(Columns, Sock) when is_list(Columns) -> + case read_packet(Sock) of + {ok, _, <<254,_/binary>>} -> []; + {ok, _, Row} -> [unpack_row(Columns, Row)|read_rows(Columns, Sock)] + end. + +unpack_row([], <<>>) -> []; +unpack_row([_|Columns], <<16#FB, Rest/binary>>) -> [undefined|unpack_row(Columns, Rest)]; +unpack_row([Column|Columns], Bin) -> + {Value, Rest} = lenenc_str(Bin), + Val = unpack_value(Column, Value), + [Val|unpack_row(Columns, Rest)]. + + +unpack_value(#column{type = 1}, <<"1">>) -> true; +unpack_value(#column{type = 1}, <<"0">>) -> false; + +unpack_value(#column{type = T}, Bin) when + T == 1; T == 2; T == 3; T == 8; T == 9; T == 13 -> + % Len = Length*8, + % <<Val:Len/little>> = Bin, + list_to_integer(binary_to_list(Bin)); + +unpack_value(_, Bin) -> + Bin. + + + +lenenc_str(<<Len, Value:Len/binary, Bin/binary>>) when Len < 251 -> {Value, Bin}; +lenenc_str(<<252, Len:16/little, Value:Len/binary, Bin/binary>>) -> {Value, Bin}; +lenenc_str(<<253, Len:24/little, Value:Len/binary, Bin/binary>>) -> {Value, Bin}. + + +varint(<<16#fe, Data:64/little, Rest/binary>>) -> {Data, Rest}; +varint(<<16#fd, Data:32/little, Rest/binary>>) -> {Data, Rest}; +varint(<<16#fc, Data:16/little, Rest/binary>>) -> {Data, Rest}; +varint(<<Data, Rest/binary>>) -> {Data, Rest}. + + +read_packet(Sock) -> + {ok, <<Len:24/little, Sequence>>} = gen_tcp:recv(Sock, 4), + case gen_tcp:recv(Sock, Len) of + {ok, <<255, Code:16/little, Error/binary>>} -> {error, {Code, Error}}; + {ok, Bin} -> {ok, Sequence, Bin} + end. + + +send_packet(Sock, Number, Bin) -> + case iolist_size(Bin) of + Size when Size < 16#ffffff -> + % io:format("out packet: ~p\n", [iolist_to_binary(Bin)]), + ok = gen_tcp:send(Sock, [<<Size:24/unsigned-little, Number>>, Bin]); + _ -> + <<Command, Rest/binary>> = iolist_to_binary(Bin), + send_multi_packet(Sock, Number, Command, Rest) + end. + +send_multi_packet(Sock, Number, Command, <<Bin:16#fffffe/binary, Rest/binary>>) -> + ok = gen_tcp:send(Sock, [<<16#ffffff:24/unsigned-little, Number, Command>>, Bin]), + send_multi_packet(Sock, Number, Command, Rest); + +send_multi_packet(Sock, Number, Command, Bin) -> + Size = size(Bin) + 1, + ok = gen_tcp:send(Sock, [<<Size:24/unsigned-little, Number, Command>>, Bin]). + + + + + + + diff --git a/test/delete_test.erl b/test/delete_test.erl deleted file mode 100644 index 8baf3e5..0000000 --- a/test/delete_test.erl +++ /dev/null @@ -1,34 +0,0 @@ -%% -*- erlang; utf-8 -*- --module(delete_test). --author('bombadil@bosqueviejo.net'). - --compile(export_all). - -% required for eunit to work --include_lib("eunit/include/eunit.hrl"). - --include("sql.hrl"). - -%%==================================================================== -%% Test cases -%%==================================================================== - -delete_simple_test() -> - ?assertEqual(mysql:parse("delete from mitabla"), - #delete{table=#table{name = <<"mitabla">>, alias = <<"mitabla">>}} - ), - ok. - -delete_where_test() -> - ?assertEqual(mysql:parse("delete from mitabla where dato='this ain''t a love song'"), - #delete{ - table=#table{name = <<"mitabla">>, alias = <<"mitabla">>}, - conditions=#condition{ - nexo=eq, - op1=#key{name = <<"dato">>, alias = <<"dato">>}, - op2=#value{value = <<"this ain't a love song">>} - } - } - ), - ok. - diff --git a/test/insert_test.erl b/test/insert_test.erl deleted file mode 100644 index 66aaff6..0000000 --- a/test/insert_test.erl +++ /dev/null @@ -1,45 +0,0 @@ -%% -*- erlang; utf-8 -*- --module(insert_test). --author('bombadil@bosqueviejo.net'). - --compile(export_all). - -% required for eunit to work --include_lib("eunit/include/eunit.hrl"). - --include("sql.hrl"). - -%%==================================================================== -%% Test cases -%%==================================================================== - -insert_simple_test() -> - ?assertEqual( - #insert{table = #table{name = <<"mitabla">>, alias = <<"mitabla">>}, values=[ - #value{value=1}, #value{value=2}, #value{value=3} - ]}, - mysql:parse("insert into mitabla values (1,2,3)") - ), - ok. - -insert_keys_test() -> - ?assertEqual( - #insert{table = #table{name = <<"mitabla">>, - alias = <<"mitabla">>}, - values = [#set{key = <<"id">>, - value = #value{value = 1}}, - #set{key = <<"author">>, - value = #value{value = <<"bonjovi">>}}, - #set{key = <<"song">>, - value = #value{value = <<"these days">>}}]}, - mysql:parse("insert into mitabla(id,author,song) values(1,'bonjovi', 'these days')") - ), - ok. - -insert_set_test() -> - ?assertEqual( - mysql:parse("insert into mitabla(id,author,song) values(1,'bonjovi', 'these days')"), - mysql:parse("insert into mitabla set id=1, author='bonjovi', song='these days'") - ), - ok. - diff --git a/test/select_test.erl b/test/select_test.erl deleted file mode 100644 index 65e0457..0000000 --- a/test/select_test.erl +++ /dev/null @@ -1,348 +0,0 @@ -%% -*- erlang; utf-8 -*- --module(select_test). --author('bombadil@bosqueviejo.net'). - --compile(export_all). - -% required for eunit to work --include_lib("eunit/include/eunit.hrl"). - --include("sql.hrl"). - -%%==================================================================== -%% Test cases -%%==================================================================== - -select_all_test() -> - ?assertEqual(mysql:parse("select *"), #select{params=[#all{}]}), - ?assertEqual(mysql:parse("SELECT *"), #select{params=[#all{}]}), - ?assertEqual(mysql:parse(" Select * "), #select{params=[#all{}]}), - ok. - -select_strings_test() -> - ?assertEqual(mysql:parse("select 'hola''mundo'"), - #select{params = [#value{value = <<"hola'mundo">>}]} - ), - ok. - -select_simple_test() -> - ?assertEqual(mysql:parse("select 'hi' as message"), - #select{params=[#value{name = <<"message">>,value = <<"hi">>}]} - ), - ?assertEqual(mysql:parse("select 'hi'"), - #select{params=[#value{value = <<"hi">>}]} - ), - ?assertEqual(mysql:parse("select hi"), - #select{params=[#key{alias = <<"hi">>,name = <<"hi">>}]} - ), - ?assertEqual(mysql:parse("select hi as hello"), - #select{params=[#key{alias = <<"hello">>,name = <<"hi">>}]} - ), - ?assertEqual(mysql:parse("select a.hi"), - #select{params=[#key{alias = <<"hi">>,name = <<"hi">>,table = <<"a">>}]} - ), - ?assertEqual(mysql:parse("select aa.hi as hello"), - #select{params=[#key{alias = <<"hello">>,name = <<"hi">>,table = <<"aa">>}]} - ), - ok. - -select_simple_multiparams_test() -> - ?assertEqual(mysql:parse("select 'hi' as message, 1 as id"), - #select{params=[#value{name = <<"message">>,value = <<"hi">>},#value{name = <<"id">>,value=1}]} - ), - ?assertEqual(mysql:parse("select 'hi', 1"), - #select{params=[#value{value = <<"hi">>},#value{value=1}]} - ), - ?assertEqual(mysql:parse("select hi, message"), - #select{params=[#key{alias = <<"hi">>,name = <<"hi">>}, - #key{alias = <<"message">>,name = <<"message">>}]} - ), - ?assertEqual(mysql:parse("select hi as hello, message as msg"), - #select{params=[#key{alias = <<"hello">>,name = <<"hi">>}, - #key{alias = <<"msg">>,name = <<"message">>}]} - ), - ?assertEqual(mysql:parse("select a.hi, a.message"), - #select{params=[#key{alias = <<"hi">>,name = <<"hi">>,table = <<"a">>}, - #key{alias = <<"message">>,name = <<"message">>,table = <<"a">>}]} - ), - ?assertEqual(mysql:parse("select aa.hi as hello, aa.message as msg"), - #select{params=[#key{alias = <<"hello">>,name = <<"hi">>,table = <<"aa">>}, - #key{alias = <<"msg">>,name = <<"message">>,table = <<"aa">>}]} - ), - ?assertEqual(mysql:parse("select a.*, b.*"), - #select{params=[#all{table = <<"a">>}, #all{table = <<"b">>}]} - ), - ?assertEqual(mysql:parse("select *, a.*, b.*"), - #select{params=[#all{}, #all{table = <<"a">>}, #all{table = <<"b">>}]} - ), - ok. - -select_simple_subquery_test() -> - ?assertEqual(mysql:parse("select (select *)"), - #select{params=[#subquery{subquery=#select{params=[#all{}]}}]} - ), - ?assertEqual(mysql:parse("select (select *), id"), - #select{params=[#subquery{subquery=#select{params=[#all{}]}}, - #key{alias = <<"id">>,name = <<"id">>}]} - ), - ?assertEqual(mysql:parse("select (select uno) as uno, dos"), - #select{params=[#subquery{name = <<"uno">>, - subquery=#select{params=[#key{alias = <<"uno">>,name = <<"uno">>}]}}, - #key{alias = <<"dos">>,name = <<"dos">>}]} - ), - ok. - -select_from_test() -> - ?assertEqual(mysql:parse("select * from data"), - #select{params=[#all{}],tables=[#table{name = <<"data">>,alias = <<"data">>}]} - ), - ?assertEqual(mysql:parse("select uno, dos from data, data2"), - #select{params = [#key{alias = <<"uno">>,name = <<"uno">>}, - #key{alias = <<"dos">>,name = <<"dos">>}], - tables = [#table{name = <<"data">>,alias = <<"data">>}, - #table{name = <<"data2">>,alias = <<"data2">>}]} - ), - ?assertEqual(mysql:parse("select d.uno, d2.dos from data as d, data2 as d2"), - #select{params = [#key{alias = <<"uno">>,name = <<"uno">>, - table = <<"d">>}, - #key{alias = <<"dos">>,name = <<"dos">>,table = <<"d2">>}], - tables = [#table{name = <<"data">>,alias = <<"d">>}, - #table{name = <<"data2">>,alias = <<"d2">>}]} - ), - ok. - -select_from_subquery_test() -> - ?assertEqual(mysql:parse("select * from (select 1 as uno,2 as dos)"), - #select{ - params = [#all{}], - tables = - [#subquery{ - subquery = - #select{ - params = - [#value{name = <<"uno">>,value = 1}, - #value{name = <<"dos">>,value = 2}]}}]} - ), - ?assertEqual(mysql:parse("select (select 1) as id, t.uno from (select 2) as t"), - #select{ - params = - [#subquery{ - name = <<"id">>, - subquery = - #select{ - params = [#value{name = undefined,value = 1}]}}, - #key{alias = <<"uno">>,name = <<"uno">>,table = <<"t">>}], - tables = - [#subquery{ - name = <<"t">>, - subquery = - #select{ - params = [#value{value = 2}]}}]} - ), - ?assertEqual(mysql:parse("select * from clientes where id in ( 1, 2, 3 )"), - #select{params = [#all{}], - tables = [#table{name = <<"clientes">>, - alias = <<"clientes">>}], - conditions = #condition{nexo = in, - op1 = #key{alias = <<"id">>,name = <<"id">>}, - op2 = #subquery{subquery = [1,2,3]}}} - ), - ok. - -select_where_test() -> - ?assertEqual(mysql:parse("select * from tabla where uno=1"), - #select{params = [#all{}], - tables = [#table{name = <<"tabla">>,alias = <<"tabla">>}], - conditions = #condition{nexo = eq, - op1 = #key{alias = <<"uno">>,name = <<"uno">>}, - op2 = #value{value = 1}}} - ), - ?assertEqual(mysql:parse("select * from tabla where uno=1 and dos<2"), - #select{ - params = [#all{}], - tables = [#table{name = <<"tabla">>,alias = <<"tabla">>}], - conditions = - #condition{ - nexo = nexo_and, - op1 = - #condition{ - nexo = eq, - op1 = - #key{alias = <<"uno">>,name = <<"uno">>}, - op2 = #value{value = 1}}, - op2 = - #condition{ - nexo = lt, - op1 = - #key{alias = <<"dos">>,name = <<"dos">>}, - op2 = #value{value = 2}}}} - ), - ?assertEqual(mysql:parse("select * from tabla where uno=1 and dos<2 and tres>3"), - #select{ - params = [#all{}], - tables = [#table{name = <<"tabla">>,alias = <<"tabla">>}], - conditions = - #condition{ - nexo = nexo_and, - op1 = - #condition{ - nexo = eq, - op1 = - #key{alias = <<"uno">>,name = <<"uno">>}, - op2 = #value{value = 1}}, - op2 = - #condition{ - nexo = nexo_and, - op1 = - #condition{ - nexo = lt, - op1 = - #key{alias = <<"dos">>,name = <<"dos">>}, - op2 = #value{value = 2}}, - op2 = - #condition{ - nexo = gt, - op1 = - #key{alias = <<"tres">>,name = <<"tres">>}, - op2 = #value{value = 3}}}}} - ), - ?assertEqual( - mysql:parse("select * from tabla where uno=1 and dos<=2 and tres>=3"), - mysql:parse("select * from tabla where uno=1 and (dos=<2 and tres=>3)") - ), - ?assertEqual( - mysql:parse("select * from a where (a=1 and b=2) and c=3"), - #select{ - params = [#all{}], - tables = [#table{name = <<"a">>,alias = <<"a">>}], - conditions = - #condition{ - nexo = nexo_and, - op1 = - #condition{ - nexo = nexo_and, - op1 = - #condition{ - nexo = eq, - op1 = #key{alias = <<"a">>,name = <<"a">>}, - op2 = #value{value = 1}}, - op2 = - #condition{ - nexo = eq, - op1 = #key{alias = <<"b">>,name = <<"b">>}, - op2 = #value{value = 2}}}, - op2 = - #condition{ - nexo = eq, - op1 = #key{alias = <<"c">>,name = <<"c">>}, - op2 = #value{value = 3}}}} - ), - ok. - -select_function_test() -> - ?assertEqual(mysql:parse("select count(*)"), - #select{params = [#function{name = <<"count">>, params = [#all{}]}]} - ), - ?assertEqual(mysql:parse("select concat('hola', 'mundo')"), - #select{params = [#function{name = <<"concat">>, - params = [#value{value = <<"hola">>}, - #value{value = <<"mundo">>}]}]} - ), - ok. - -select_groupby_test() -> - ?assertEqual(mysql:parse("select fecha, count(*) as total from datos group by fecha"), - #select{params = [#key{alias = <<"fecha">>, - name = <<"fecha">>}, - #function{name = <<"count">>, - params = [#all{}], - alias = <<"total">>}], - tables = [#table{name = <<"datos">>,alias = <<"datos">>}], - group = [<<"fecha">>]} - ), - ?assertEqual(mysql:parse("select fecha, count(*) from datos group by fecha"), - #select{params = [#key{alias = <<"fecha">>, - name = <<"fecha">>}, - #function{name = <<"count">>, - params = [#all{}], - alias = undefined}], - tables = [#table{name = <<"datos">>,alias = <<"datos">>}], - group = [<<"fecha">>]} - ), - ?assertEqual(mysql:parse("select * from a group by 1"), - #select{params = [#all{}], - tables = [#table{name = <<"a">>,alias = <<"a">>}], - group = [1]} - ), - ok. - -select_orderby_test() -> - ?assertEqual(mysql:parse("select * from tabla order by 1"), - #select{ - params=[#all{}], - tables=[#table{alias = <<"tabla">>, name = <<"tabla">>}], - order=[#order{key=1,sort=asc}] - } - ), - ?assertEqual(mysql:parse("select * from tabla order by 1 desc"), - #select{ - params=[#all{}], - tables=[#table{alias = <<"tabla">>, name = <<"tabla">>}], - order=[#order{key=1,sort=desc}] - } - ), - ok. - -select_limit_test() -> - ?assertEqual(mysql:parse("select * from tabla limit 10"), - #select{ - params=[#all{}], - tables=[#table{alias = <<"tabla">>, name = <<"tabla">>}], - limit=10 - } - ), - ?assertEqual(mysql:parse("select * from tabla limit 10 offset 5"), - #select{ - params=[#all{}], - tables=[#table{alias = <<"tabla">>, name = <<"tabla">>}], - limit=10, - offset=5 - } - ), - ok. - -select_arithmetic_test() -> - ?assertEqual( - #select{params = [#operation{type = <<"+">>, - op1 = #value{value = 2}, - op2 = #value{value = 3}}]}, - mysql:parse("select 2+3") - ), - ?assertEqual( - mysql:parse("select 2+3"), - mysql:parse("select (2+3)") - ), - ?assertNotEqual( - mysql:parse("select (2+3)*4"), - mysql:parse("select 2+3*4") - ), - ?assertEqual( - #select{params = [#operation{type = <<"*">>, - op1 = #operation{type = <<"+">>, - op1 = #value{value = 2}, - op2 = #value{value = 3}}, - op2 = #value{value = 4}}]}, - mysql:parse("select (2+3)*4") - ), - ?assertEqual( - #select{params = [#all{}], - tables = [#table{name = <<"data">>,alias = <<"data">>}], - conditions = #condition{nexo = eq, - op1 = #key{alias = <<"a">>,name = <<"a">>}, - op2 = #operation{type = <<"*">>, - op1 = #key{alias = <<"b">>,name = <<"b">>}, - op2 = #value{value = 3}}}}, - mysql:parse("select * from data where a = b*3") - ), - ok. - diff --git a/test/sql_SUITE.erl b/test/sql_SUITE.erl new file mode 100644 index 0000000..568ebe0 --- /dev/null +++ b/test/sql_SUITE.erl @@ -0,0 +1,699 @@ +-module(sql_SUITE). + +% -include("../include/sql.hrl"). +-include("../include/myproto.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-compile(export_all). + + +all() -> + [{group,parse}]. + + +groups() -> + [{parse, [parallel], [ + delete_simple, + delete_where, + insert_simple, + insert_keys, + insert_set, + describe, + show, + show_like, + transaction, + set, + select_all, + select_strings, + select_simple, + select_simple_multiparams, + select_simple_subquery, + select_from, + select_from_subquery, + select_where, + select_function, + select_groupby, + select_orderby, + select_limit, + select_arithmetic, + select_variable, + select_in, + + server_select_simple, + server_reject_password, + server_very_long_query, + update_simple, + update_multiparams, + update_where, + long_query_2 + ]}]. + + +init_per_suite(Config) -> application:ensure_all_started(ranch), Config. +end_per_suite(Config) -> Config. + +transaction(_) -> + 'begin' = mysql_proto:parse("begin"), + 'commit' = mysql_proto:parse("commit"), + 'rollback' = mysql_proto:parse("rollback"), + ok. + + +describe(_) -> + #describe{table = #table{name = <<"streams">>}} = mysql_proto:parse("DESCRIBE `streams`"). + +delete_simple(_) -> + #delete{table=#table{name = <<"mitabla">>, alias = <<"mitabla">>}} = mysql_proto:parse("delete from mitabla"). + + +delete_where(_) -> + #delete{ + table=#table{name = <<"mitabla">>, alias = <<"mitabla">>}, + conditions=#condition{ + nexo=eq, + op1=#key{name = <<"dato">>, alias = <<"dato">>}, + op2=#value{value = <<"this ain't a love song">>} + } + } = mysql_proto:parse("delete from mitabla where dato='this ain''t a love song'"). + + + +insert_simple(_) -> + #insert{table = #table{name = <<"mitabla">>, alias = <<"mitabla">>}, values=[ + #value{value=1}, #value{value=2}, #value{value=3} + ]} = mysql_proto:parse("insert into mitabla values (1,2,3)"). + + +insert_keys(_) -> + #insert{table = #table{name = <<"mitabla">>, + alias = <<"mitabla">>}, + values = [#set{key = <<"id">>, + value = #value{value = 1}}, + #set{key = <<"author">>, + value = #value{value = <<"bonjovi">>}}, + #set{key = <<"song">>, + value = #value{value = <<"these days">>}}]} = + mysql_proto:parse("insert into mitabla(id,author,song) values(1,'bonjovi', 'these days')"). + + +insert_set(_) -> + A = mysql_proto:parse("insert into mitabla(id,author,song) values(1,'bonjovi', 'these days')"), + B = mysql_proto:parse("insert into mitabla set id=1, author='bonjovi', song='these days'"), + A = B. + + + + +show(_) -> + #show{type=databases} = mysql_proto:parse("SHOW databases"), + #show{type=variables} = mysql_proto:parse("SHOW variables"), + #show{type=tables, full = true} = mysql_proto:parse("SHOW FULL tables"), + #show{type=tables, full = false} = mysql_proto:parse("SHOW tables"), + #show{type=fields,full=true,from= <<"streams">>} = mysql_proto:parse("SHOW FULL FIELDS FROM `streams`"), + #show{type=fields,full=false,from= <<"streams">>} = mysql_proto:parse("SHOW FIELDS FROM `streams`"), + #show{type=tables,full=false,from= {like,<<"streams">>}} = mysql_proto:parse("SHOW TABLES LIKE 'streams'"), + #show{type=create_table,from= <<"streams">>} = mysql_proto:parse("SHOW CREATE TABLE `streams`"), + #show{type=variables, conditions=#condition{ + nexo = eq, + op1 = #key{name = <<"Variable_name">>}, + op2 = #value{value = <<"character_set_client">>} + }} = mysql_proto:parse("SHOW VARIABLES WHERE Variable_name = 'character_set_client'"), + + #show{type=collation, conditions=#condition{ + nexo = eq, + op1 = #key{name= <<"Charset">>}, + op2 = #value{value = <<"utf8">>} + }} = mysql_proto:parse("show collation where Charset = 'utf8'"), + ok. + + +show_like(_) -> + #show{type=variables, conditions = {like, <<"sql_mode">>}} = mysql_proto:parse("SHOW VARIABLES LIKE 'sql_mode'"), + ok. + + + +set(_) -> + #system_set{query=[{#variable{name = <<"a">>, scope = session},0}]} = mysql_proto:parse("SET a=0"), + #system_set{query=[{#variable{name = <<"NAMES">>},<<"utf8">>}]} = mysql_proto:parse("SET NAMES 'utf8'"), + #system_set{query=[{#variable{name = <<"NAMES">>},<<"utf8">>}]} = mysql_proto:parse("SET NAMES utf8"), + + #system_set{query=[ + {#variable{name = <<"SQL_AUTO_IS_NULL">>, scope = session},0}, + {#variable{name = <<"NAMES">>},<<"utf8">>}, + {#variable{name = <<"wait_timeout">>, scope = local}, 2147483} + ]} = mysql_proto:parse("SET SQL_AUTO_IS_NULL=0, NAMES 'utf8', @@wait_timeout = 2147483"), + ok. + + +select_variable(_) -> + #select{params = [#variable{name = <<"max_allowed_packet">>, scope = local}]} = + mysql_proto:parse("SELECT @@max_allowed_packet"), + #select{params = [#variable{name = <<"global.max_allowed_packet">>, scope = local}]} = + mysql_proto:parse("SELECT @@global.max_allowed_packet"), + ok. + + +select_in(_) -> + #select{params=[#all{}], + conditions=#condition{nexo=in, + op1 = #key{name= <<"n">>}, + op2 = #subquery{subquery = [<<"a">>,<<"b">>]} + } + } = mysql_proto:parse(<<"SELECT * from b where n in ('a','b') order by a">>), + #select{params=[#all{}], + conditions=#condition{nexo=not_in, + op1 = #key{name= <<"n">>}, + op2 = #subquery{subquery = [<<"a">>,<<"b">>]} + } + } = mysql_proto:parse(<<"SELECT * from b where n not in ('a','b') order by a">>), + ok. + + +select_all(_) -> + #select{params=[#all{}]} = mysql_proto:parse("select *"), + #select{params=[#all{}]} = mysql_proto:parse("SELECT *"), + #select{params=[#all{}]} = mysql_proto:parse(" Select * "), + ok. + +select_strings(_) -> + #select{params = [#value{value = <<"hola'mundo">>}]} = mysql_proto:parse("select 'hola''mundo'"). + + +select_simple(_) -> + ?assertEqual(mysql_proto:parse("select 'hi' as message"), + #select{params=[#value{name = <<"message">>,value = <<"hi">>}]} + ), + ?assertEqual(mysql_proto:parse("select 'hi'"), + #select{params=[#value{value = <<"hi">>}]} + ), + ?assertEqual(mysql_proto:parse("select hi"), + #select{params=[#key{alias = <<"hi">>,name = <<"hi">>}]} + ), + ?assertEqual(mysql_proto:parse("select hi as hello"), + #select{params=[#key{alias = <<"hello">>,name = <<"hi">>}]} + ), + ?assertEqual(mysql_proto:parse("select a.hi"), + #select{params=[#key{alias = <<"hi">>,name = <<"hi">>,table = <<"a">>}]} + ), + ?assertEqual(mysql_proto:parse("select aa.hi as hello"), + #select{params=[#key{alias = <<"hello">>,name = <<"hi">>,table = <<"aa">>}]} + ), + ok. + +select_simple_multiparams(_) -> + ?assertEqual(mysql_proto:parse("select 'hi' as message, 1 as id"), + #select{params=[#value{name = <<"message">>,value = <<"hi">>},#value{name = <<"id">>,value=1}]} + ), + ?assertEqual(mysql_proto:parse("select 'hi', 1"), + #select{params=[#value{value = <<"hi">>},#value{value=1}]} + ), + ?assertEqual(mysql_proto:parse("select hi, message"), + #select{params=[#key{alias = <<"hi">>,name = <<"hi">>}, + #key{alias = <<"message">>,name = <<"message">>}]} + ), + ?assertEqual(mysql_proto:parse("select hi as hello, message as msg"), + #select{params=[#key{alias = <<"hello">>,name = <<"hi">>}, + #key{alias = <<"msg">>,name = <<"message">>}]} + ), + ?assertEqual(mysql_proto:parse("select a.hi, a.message"), + #select{params=[#key{alias = <<"hi">>,name = <<"hi">>,table = <<"a">>}, + #key{alias = <<"message">>,name = <<"message">>,table = <<"a">>}]} + ), + ?assertEqual(mysql_proto:parse("select aa.hi as hello, aa.message as msg"), + #select{params=[#key{alias = <<"hello">>,name = <<"hi">>,table = <<"aa">>}, + #key{alias = <<"msg">>,name = <<"message">>,table = <<"aa">>}]} + ), + ?assertEqual(mysql_proto:parse("select a.*, b.*"), + #select{params=[#all{table = <<"a">>}, #all{table = <<"b">>}]} + ), + ?assertEqual(mysql_proto:parse("select *, a.*, b.*"), + #select{params=[#all{}, #all{table = <<"a">>}, #all{table = <<"b">>}]} + ), + ok. + +select_simple_subquery(_) -> + ?assertEqual(mysql_proto:parse("select (select *)"), + #select{params=[#subquery{subquery=#select{params=[#all{}]}}]} + ), + ?assertEqual(mysql_proto:parse("select (select *), id"), + #select{params=[#subquery{subquery=#select{params=[#all{}]}}, + #key{alias = <<"id">>,name = <<"id">>}]} + ), + ?assertEqual(mysql_proto:parse("select (select uno) as uno, dos"), + #select{params=[#subquery{name = <<"uno">>, + subquery=#select{params=[#key{alias = <<"uno">>,name = <<"uno">>}]}}, + #key{alias = <<"dos">>,name = <<"dos">>}]} + ), + ok. + +select_from(_) -> + ?assertEqual(mysql_proto:parse("select * from data"), + #select{params=[#all{}],tables=[#table{name = <<"data">>,alias = <<"data">>}]} + ), + ?assertEqual(mysql_proto:parse("select uno, dos from data, data2"), + #select{params = [#key{alias = <<"uno">>,name = <<"uno">>}, + #key{alias = <<"dos">>,name = <<"dos">>}], + tables = [#table{name = <<"data">>,alias = <<"data">>}, + #table{name = <<"data2">>,alias = <<"data2">>}]} + ), + ?assertEqual(mysql_proto:parse("select d.uno, d2.dos from data as d, data2 as d2"), + #select{params = [#key{alias = <<"uno">>,name = <<"uno">>, + table = <<"d">>}, + #key{alias = <<"dos">>,name = <<"dos">>,table = <<"d2">>}], + tables = [#table{name = <<"data">>,alias = <<"d">>}, + #table{name = <<"data2">>,alias = <<"d2">>}]} + ), + + #select{params = [#all{}], tables =[#table{name= <<"streams">>}], + order = [#order{key= <<"name">>, sort = asc}], limit =1} = + mysql_proto:parse("SELECT `streams`.* FROM `streams` ORDER BY `streams`.`name` ASC LIMIT 1"), + ok. + +select_from_subquery(_) -> + ?assertEqual(mysql_proto:parse("select * from (select 1 as uno,2 as dos)"), + #select{ + params = [#all{}], + tables = + [#subquery{ + subquery = + #select{ + params = + [#value{name = <<"uno">>,value = 1}, + #value{name = <<"dos">>,value = 2}]}}]} + ), + ?assertEqual(mysql_proto:parse("select (select 1) as id, t.uno from (select 2) as t"), + #select{ + params = + [#subquery{ + name = <<"id">>, + subquery = + #select{ + params = [#value{name = undefined,value = 1}]}}, + #key{alias = <<"uno">>,name = <<"uno">>,table = <<"t">>}], + tables = + [#subquery{ + name = <<"t">>, + subquery = + #select{ + params = [#value{value = 2}]}}]} + ), + ?assertEqual(mysql_proto:parse("select * from clientes where id in ( 1, 2, 3 )"), + #select{params = [#all{}], + tables = [#table{name = <<"clientes">>, + alias = <<"clientes">>}], + conditions = #condition{nexo = in, + op1 = #key{alias = <<"id">>,name = <<"id">>}, + op2 = #subquery{subquery = [1,2,3]}}} + ), + ok. + +select_where(_) -> + ?assertEqual(mysql_proto:parse("select * from tabla where uno=1"), + #select{params = [#all{}], + tables = [#table{name = <<"tabla">>,alias = <<"tabla">>}], + conditions = #condition{nexo = eq, + op1 = #key{alias = <<"uno">>,name = <<"uno">>}, + op2 = #value{value = 1}}} + ), + ?assertEqual(mysql_proto:parse("select * from tabla where uno=1 and dos<2"), + #select{ + params = [#all{}], + tables = [#table{name = <<"tabla">>,alias = <<"tabla">>}], + conditions = + #condition{ + nexo = nexo_and, + op1 = + #condition{ + nexo = eq, + op1 = + #key{alias = <<"uno">>,name = <<"uno">>}, + op2 = #value{value = 1}}, + op2 = + #condition{ + nexo = lt, + op1 = + #key{alias = <<"dos">>,name = <<"dos">>}, + op2 = #value{value = 2}}}} + ), + ?assertEqual(mysql_proto:parse("select * from tabla where uno=1 and dos<2 and tres>3"), + #select{ + params = [#all{}], + tables = [#table{name = <<"tabla">>,alias = <<"tabla">>}], + conditions = + #condition{ + nexo = nexo_and, + op1 = + #condition{ + nexo = eq, + op1 = + #key{alias = <<"uno">>,name = <<"uno">>}, + op2 = #value{value = 1}}, + op2 = + #condition{ + nexo = nexo_and, + op1 = + #condition{ + nexo = lt, + op1 = + #key{alias = <<"dos">>,name = <<"dos">>}, + op2 = #value{value = 2}}, + op2 = + #condition{ + nexo = gt, + op1 = + #key{alias = <<"tres">>,name = <<"tres">>}, + op2 = #value{value = 3}}}}} + ), + ?assertEqual( + mysql_proto:parse("select * from tabla where uno=1 and dos<=2 and tres>=3"), + mysql_proto:parse("select * from tabla where uno=1 and (dos=<2 and tres=>3)") + ), + ?assertEqual( + mysql_proto:parse("select * from a where (a=1 and b=2) and c=3"), + #select{ + params = [#all{}], + tables = [#table{name = <<"a">>,alias = <<"a">>}], + conditions = + #condition{ + nexo = nexo_and, + op1 = + #condition{ + nexo = nexo_and, + op1 = + #condition{ + nexo = eq, + op1 = #key{alias = <<"a">>,name = <<"a">>}, + op2 = #value{value = 1}}, + op2 = + #condition{ + nexo = eq, + op1 = #key{alias = <<"b">>,name = <<"b">>}, + op2 = #value{value = 2}}}, + op2 = + #condition{ + nexo = eq, + op1 = #key{alias = <<"c">>,name = <<"c">>}, + op2 = #value{value = 3}}}} + ), + ok. + +select_function(_) -> + ?assertEqual(mysql_proto:parse("select count(*)"), + #select{params = [#function{name = <<"count">>, params = [#all{}]}]} + ), + ?assertEqual(mysql_proto:parse("select concat('hola', 'mundo')"), + #select{params = [#function{name = <<"concat">>, + params = [#value{value = <<"hola">>}, + #value{value = <<"mundo">>}]}]} + ), + ok. + +select_groupby(_) -> + ?assertEqual(mysql_proto:parse("select fecha, count(*) as total from datos group by fecha"), + #select{params = [#key{alias = <<"fecha">>, + name = <<"fecha">>}, + #function{name = <<"count">>, + params = [#all{}], + alias = <<"total">>}], + tables = [#table{name = <<"datos">>,alias = <<"datos">>}], + group = [<<"fecha">>]} + ), + ?assertEqual(mysql_proto:parse("select fecha, count(*) from datos group by fecha"), + #select{params = [#key{alias = <<"fecha">>, + name = <<"fecha">>}, + #function{name = <<"count">>, + params = [#all{}], + alias = undefined}], + tables = [#table{name = <<"datos">>,alias = <<"datos">>}], + group = [<<"fecha">>]} + ), + ?assertEqual(mysql_proto:parse("select * from a group by 1"), + #select{params = [#all{}], + tables = [#table{name = <<"a">>,alias = <<"a">>}], + group = [1]} + ), + ok. + +select_orderby(_) -> + ?assertEqual(mysql_proto:parse("select * from tabla order by 1"), + #select{ + params=[#all{}], + tables=[#table{alias = <<"tabla">>, name = <<"tabla">>}], + order=[#order{key=1,sort=asc}] + } + ), + ?assertEqual(mysql_proto:parse("select * from tabla order by 1 desc"), + #select{ + params=[#all{}], + tables=[#table{alias = <<"tabla">>, name = <<"tabla">>}], + order=[#order{key=1,sort=desc}] + } + ), + ok. + +select_limit(_) -> + ?assertEqual(mysql_proto:parse("select * from tabla limit 10"), + #select{ + params=[#all{}], + tables=[#table{alias = <<"tabla">>, name = <<"tabla">>}], + limit=10 + } + ), + ?assertEqual(mysql_proto:parse("select * from tabla limit 10 offset 5"), + #select{ + params=[#all{}], + tables=[#table{alias = <<"tabla">>, name = <<"tabla">>}], + limit=10, + offset=5 + } + ), + ok. + +select_arithmetic(_) -> + ?assertEqual( + #select{params = [#operation{type = <<"+">>, + op1 = #value{value = 2}, + op2 = #value{value = 3}}]}, + mysql_proto:parse("select 2+3") + ), + ?assertEqual( + mysql_proto:parse("select 2+3"), + mysql_proto:parse("select (2+3)") + ), + ?assertNotEqual( + mysql_proto:parse("select (2+3)*4"), + mysql_proto:parse("select 2+3*4") + ), + ?assertEqual( + #select{params = [#operation{type = <<"*">>, + op1 = #operation{type = <<"+">>, + op1 = #value{value = 2}, + op2 = #value{value = 3}}, + op2 = #value{value = 4}}]}, + mysql_proto:parse("select (2+3)*4") + ), + ?assertEqual( + #select{params = [#all{}], + tables = [#table{name = <<"data">>,alias = <<"data">>}], + conditions = #condition{nexo = eq, + op1 = #key{alias = <<"a">>,name = <<"a">>}, + op2 = #operation{type = <<"*">>, + op1 = #key{alias = <<"b">>,name = <<"b">>}, + op2 = #value{value = 3}}}}, + mysql_proto:parse("select * from data where a = b*3") + ), + ok. + + + + + + +update_simple(_) -> + ?assertEqual( + mysql_proto:parse("update mitabla set dato=1"), + #update{ + table=#table{alias = <<"mitabla">>, name = <<"mitabla">>}, + set=[#set{key = <<"dato">>, value=#value{value=1}}] + } + ), + ?assertEqual( + mysql_proto:parse(" Update mitabla SET dato = 1 "), + mysql_proto:parse("UPDATE mitabla SET dato=1") + ), + ok. + +update_multiparams(_) -> + ?assertEqual( + mysql_proto:parse("update mitabla set dato1=1, dato2='bon jovi', dato3='this ain''t a love song'"), + #update{ + table=#table{alias = <<"mitabla">>, name = <<"mitabla">>}, + set=[ + #set{key = <<"dato1">>, value=#value{value = 1}}, + #set{key = <<"dato2">>, value=#value{value = <<"bon jovi">>}}, + #set{key = <<"dato3">>, value=#value{value = <<"this ain't a love song">>}} + ] + } + ), + ok. + +update_where(_) -> + ?assertEqual( + mysql_proto:parse("update mitabla set dato=1 where dato=5"), + #update{ + table=#table{alias = <<"mitabla">>, name = <<"mitabla">>}, + set=[#set{key = <<"dato">>, value=#value{value=1}}], + conditions=#condition{ + nexo=eq, + op1=#key{alias = <<"dato">>, name = <<"dato">>}, + op2=#value{value=5} + } + } + ), + ok. + + + + + + + + + + +server_select_simple(_) -> + {ok, LSocket} = gen_tcp:listen(0, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]), + {ok, ListenPort} = inet:port(LSocket), + Client = spawn_link(fun() -> + {ok, Sock} = nanomysql:connect("mysql://user:pass@127.0.0.1:"++integer_to_list(ListenPort)++"/dbname"), + Query1 = "SELECT input,output FROM minute_stats WHERE source='net' AND time >= '2013-09-05' AND time < '2013-09-06'", + {ok, {Columns1, Rows1}} = nanomysql:execute(Query1, Sock), + [{<<"input">>,_}, {<<"output">>,_}] = Columns1, + [ + [<<"20">>,20], + [<<"30">>,30], + [<<"40">>,undefined] + ] = Rows1, + ok + end), + erlang:monitor(process, Client), + {ok, Sock} = gen_tcp:accept(LSocket), + My0 = my_protocol:init([{socket,Sock}]), + {ok, My1} = my_protocol:hello(42, My0), + {ok, #request{info = #user{}}, My2} = my_protocol:next_packet(My1), + {ok, My3} = my_protocol:ok(My2), + + {ok, #request{info = #select{} = Select}, My4} = my_protocol:next_packet(My3), + #select{ + params = [#key{name = <<"input">>},#key{name = <<"output">>}], + tables = [#table{name = <<"minute_stats">>}], + conditions = #condition{nexo = nexo_and, + op1 = #condition{nexo = eq, op1 = #key{name = <<"source">>},op2 = #value{value = <<"net">>}}, + op2 = #condition{nexo = nexo_and, + op1 = #condition{nexo = gte, op1 = #key{name = <<"time">>}, op2 = #value{value = <<"2013-09-05">>}}, + op2 = #condition{nexo = lt, op1 = #key{name = <<"time">>}, op2 = #value{value = <<"2013-09-06">>}} + } + } + } = Select, + + ResponseFields = { + [ + #column{name = <<"input">>, type=?TYPE_VARCHAR, length=20}, + #column{name = <<"output">>, type=?TYPE_LONG, length = 8} + ], + [ + [<<"20">>, 20], + [<<"30">>, 30], + [<<"40">>, undefined] + ] + }, + Response = #response{status=?STATUS_OK, info = ResponseFields}, + {ok, _My5} = my_protocol:send_or_reply(Response, My4), + receive {'DOWN', _, _, Client, Reason} -> normal = Reason end, + ok. + + + + +server_reject_password(_) -> + {ok, LSocket} = gen_tcp:listen(0, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]), + {ok, ListenPort} = inet:port(LSocket), + Client = spawn_link(fun() -> + {error,{1045,<<"password rejected">>}} = nanomysql:connect("mysql://user:pass@127.0.0.1:"++integer_to_list(ListenPort)++"/dbname"), + ok + end), + erlang:monitor(process, Client), + {ok, Sock} = gen_tcp:accept(LSocket), + My0 = my_protocol:init([{socket,Sock}]), + {ok, My1} = my_protocol:hello(42, My0), + {ok, #request{info = #user{}}, My2} = my_protocol:next_packet(My1), + {ok, _My3} = my_protocol:error(<<"password rejected">>, My2), + + receive {'DOWN', _, _, Client, Reason} -> normal = Reason end, + ok. + + +server_very_long_query(_) -> + Value = binary:copy(<<"0123456789">>, 2177721), + Query = iolist_to_binary(["INSERT INTO photos (data) VALUES ('", Value, "')"]), + + {ok, LSocket} = gen_tcp:listen(0, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]), + {ok, ListenPort} = inet:port(LSocket), + Client = spawn_link(fun() -> + {ok, Sock} = nanomysql:connect("mysql://user:pass@127.0.0.1:"++integer_to_list(ListenPort)++"/dbname"), + nanomysql:execute(Query, Sock), + ok + end), + erlang:monitor(process, Client), + {ok, Sock} = gen_tcp:accept(LSocket), + My0 = my_protocol:init([{socket,Sock},{parse_query,false}]), + {ok, My1} = my_protocol:hello(42, My0), + {ok, #request{info = #user{}}, My2} = my_protocol:next_packet(My1), + {ok, My3} = my_protocol:ok(My2), + + {ok, #request{info = Query}, My4} = my_protocol:next_packet(My3), + + ResponseFields = { + [#column{name = <<"id">>, type=?TYPE_LONG, length = 8}], + [[20]] + }, + Response = #response{status=?STATUS_OK, info = ResponseFields}, + {ok, _My5} = my_protocol:send_or_reply(Response, My4), + + % receive {'DOWN', _, _, Client, Reason} -> normal = Reason end, + ok. + +long_query_2(_) -> + {ok,_} = test_handler:start_server(long_query_2, 0), + + Port = test_handler:existing_port(long_query_2), + ConnStr = <<"mysql://user:user@127.0.0.1:", (integer_to_binary(Port))/binary, "/test_db?login=init_db">>, + + Values0 = binary:copy(<<"'test_name',">>, 500), + Values = <<Values0/binary, "'test_name'">>, + Owner = self(), + + Pid = spawn(fun() -> + Result = + receive + start -> + {ok, Connection} = nanomysql:connect(binary_to_list(ConnStr)), + nanomysql:execute(<<"select * from test where name in (", Values/binary, ")">>, Connection), + ok + after 100 -> + {error, not_started} + end, + Owner ! Result + end), + + erlang:monitor(process, Pid), + Pid ! start, + + ok = receive + Msg -> Msg + after 1000 -> + exit(Pid, kill), + {error, timeout} + end. + + diff --git a/test/test_handler.erl b/test/test_handler.erl new file mode 100644 index 0000000..b67a71d --- /dev/null +++ b/test/test_handler.erl @@ -0,0 +1,312 @@ +-module (test_handler). +-behaviour(gen_myproto). +-include_lib("myproto/include/myproto.hrl"). + +-export([start_server/1, start_server/2, stop_server/0, stop_server/1, existing_port/1]). +-export([table_columns/1, tables/0]). +-export([check_pass/1, execute/2, terminate/2, metadata/2]). + + +-define(ERR_WRONG_PASS, {error, <<"Password incorrect!">>}). +-define(ERR_WRONG_USER, {error, <<"No such user!">>}). +-define(ERR_LOGIN_DISABLED, {error, <<"Login disabled">>}). +-define(ERR_INFO(Code, Desc), #response{status=?STATUS_ERR, error_code=Code, info=Desc}). + +existing_port(Ref) -> + Supervisors = supervisor:which_children(ranch_sup), + case lists:keyfind({ranch_listener_sup,Ref}, 1, Supervisors) of + {_, Pid, _, _} when is_pid(Pid) -> + case process_info(Pid) of + undefined -> undefined; + _ -> ranch:get_port(Ref) + end; + _ -> + undefined + end. + +start_server(HostPort) -> start_server(test_sql1, HostPort). +start_server(Name, HostPort) -> + {Host,Port} = if + is_number(HostPort) -> {[], HostPort}; + is_binary(HostPort) -> + [H,P] = binary:split(HostPort, <<":">>), + {ok, Ip} = inet_parse:address(binary_to_list(H)), + {[{ip,Ip}],binary_to_integer(P)} + end, + + case gen_tcp:listen(Port, [binary, {reuseaddr, true}, {active, false}, {backlog, 4096}] ++ Host) of + {ok, LSock} -> + ranch:start_listener(Name, 5, ranch_tcp, [{socket, LSock},{max_connections,300}], my_ranch_worker, [?MODULE, []]); + {error, eaddrinuse}=Err -> + lager:info("Cannot start MySQL server on port ~B (in use)", [Port]), Err; + {error, Error}=Err -> + lager:info("Cannot start MySQL server on port ~B (~p)", [Port, Error]), Err + end. + +stop_server() -> stop_server(test_sql1). +stop_server(Name) -> + my_ranch_worker:stop_server(Name). + + +-record(my, { + db +}). + +check_pass(#user{name = <<"user">>, password = Pass} = User) -> + {ok,Pass,#my{}}; + +check_pass(_) -> + ?ERR_WRONG_PASS. + + +metadata(version, State) -> + {reply, <<"5.6.0">>, State}; + +metadata({connect_db, <<"test_db">>}, State) -> + {noreply, State}; + +metadata(databases, State) -> + {reply, [<<"test_db">>], State}; + +metadata(tables, State) -> + {reply, {<<"test_db">>, tables()}, State}; + +metadata({fields, Table}, State) -> + {reply, {<<"test_db">>, Table, table_columns(Table)}, State}; + +metadata(_, State) -> + {noreply, State}. + + + + +tables() -> <<"test">>. +table_columns(<<"test">>) -> [{id,string},{name,string},{url,string}]. + + + +% $$\ $$\ +% $$ | \__| +% $$ | $$$$$$\ $$$$$$\ $$\ $$$$$$$\ +% $$ | $$ __$$\ $$ __$$\ $$ |$$ _____| +% $$ | $$ / $$ |$$ / $$ |$$ |$$ / +% $$ | $$ | $$ |$$ | $$ |$$ |$$ | +% $$$$$$$$\\$$$$$$ |\$$$$$$$ |$$ |\$$$$$$$\ +% \________|\______/ \____$$ |\__| \_______| +% $$\ $$ | +% \$$$$$$ | +% \______/ + +execute(#request{info = #select{tables = [#table{name = Table}]} = Select}, #my{} = State) -> + TableColumns = table_columns(Table), + Columns = [N || {N,_} <- TableColumns], + Rows = [ [{id, <<"1">>}, {name, <<"stream1">>}, {url, <<"rtsp://...">>}] ], + ResponseColumns = [ + case lists:keyfind(Name,1,table_columns(Table)) of + false -> {Name, string}; + Col -> Col + end || Name <- Columns], + Response = { response_columns(ResponseColumns), [response_row(Row, Columns) || Row <- Rows]}, + {reply, #response{status=?STATUS_OK, info = Response}, State}; + + % #select{params = Params, conditions = Conditions1} = Select, + % Conditions = normalize_condition_types(Handler:table_columns(Table), Conditions1), + + % TableColumns = Handler:table_columns(Table), + % Columns = case Params of + % [#all{}] -> [N || {N,_} <- TableColumns]; + % [#key{}|_] -> [binary_to_existing_atom(Name,latin1) || #key{name = Name} <- Params]; + % [#function{name = <<"COUNT">>}] -> [count] + % end, + + % case Handler:select(Table, Columns, Conditions) of + % {error, Code, Desc} -> + % {reply, #response{status=?STATUS_ERR, error_code=Code, info=Desc}, State}; + % Rows when Columns == [count] andalso is_list(Rows) -> + % Response = { + % [#column{name = <<"COUNT">>, type = ?TYPE_LONGLONG}], + % [[length(Rows)]] + % }, + % {reply, #response{status=?STATUS_OK, info = Response}, State}; + % {ReplyColumns, Rows} -> + % Response = {response_columns(ReplyColumns), Rows}, + % {reply, #response{status=?STATUS_OK, info = Response}, State}; + % Rows when is_list(Rows) -> + % ResponseColumns = [case lists:keyfind(Name,1,TableColumns) of + % false -> {Name, string}; + % Col -> Col + % end || Name <- Columns], + % Response = { response_columns(ResponseColumns), [response_row(Row, Columns) || Row <- Rows]}, + % {reply, #response{status=?STATUS_OK, info = Response}, State} + % end; + + +execute(#request{} = _Request, #my{} = State) -> + {reply, default, State}. + +% execute(#request{command = 'query', info = #system_set{}} = _Request, #my{} = State) -> +% {reply, default, State}; + +% execute(#request{command = 'query', info = {use,_}} = _Request, #my{} = State) -> +% {reply, default, State}; + +% execute(#request{info = #select{params=[#variable{}]}} = _Request, #my{} = State) -> +% {reply, default, State}; + +% execute(#request{command = 'query'}, #my{handler = undefined} = State) -> +% {reply, #response{status=?STATUS_ERR, error_code=1046, info = <<"No database selected">>}, State}; + +% execute(#request{command = 'query'} = Request, #my{} = State) -> + +% run_query(Request, State); + +% execute(_Default, State) -> +% % lager:info("default: ~p", [_Default]), +% {reply, default, State}. + + +% run_query(#request{info = 'begin'}, #my{} = State) -> +% {reply, default, State}; + +% run_query(#request{info = commit}, #my{} = State) -> +% {reply, default, State}; + +% run_query(#request{info = rollback}, #my{} = State) -> +% {reply, default, State}; + +% run_query(#request{info = #select{params = [#function{name = <<"DATABASE">>}]}}, #my{db = Database} = State) -> +% {reply, #response{status=?STATUS_OK, info = { +% [#column{name = <<"database">>, type = ?TYPE_VAR_STRING, length = 20}], +% [[Database]] +% }}, State}; + +% run_query(#request{info = #select{tables = [#table{name = Table}]} = Select}, #my{handler = Handler} = State) -> +% #select{params = Params, conditions = Conditions1} = Select, +% Conditions = normalize_condition_types(Handler:table_columns(Table), Conditions1), + +% TableColumns = Handler:table_columns(Table), +% Columns = case Params of +% [#all{}] -> [N || {N,_} <- TableColumns]; +% [#key{}|_] -> [binary_to_existing_atom(Name,latin1) || #key{name = Name} <- Params]; +% [#function{name = <<"COUNT">>}] -> [count] +% end, + +% case Handler:select(Table, Columns, Conditions) of +% {error, Code, Desc} -> +% {reply, #response{status=?STATUS_ERR, error_code=Code, info=Desc}, State}; +% Rows when Columns == [count] andalso is_list(Rows) -> +% Response = { +% [#column{name = <<"COUNT">>, type = ?TYPE_LONGLONG}], +% [[length(Rows)]] +% }, +% {reply, #response{status=?STATUS_OK, info = Response}, State}; +% {ReplyColumns, Rows} -> +% Response = {response_columns(ReplyColumns), Rows}, +% {reply, #response{status=?STATUS_OK, info = Response}, State}; +% Rows when is_list(Rows) -> +% ResponseColumns = [case lists:keyfind(Name,1,TableColumns) of +% false -> {Name, string}; +% Col -> Col +% end || Name <- Columns], +% Response = { response_columns(ResponseColumns), [response_row(Row, Columns) || Row <- Rows]}, +% {reply, #response{status=?STATUS_OK, info = Response}, State} +% end; + +% run_query(#request{info = #insert{table = #table{name = Table}, values = ValuesSpec}}, #my{handler = Handler, role = admin} = State) -> +% case erlang:function_exported(Handler, insert, 2) of +% false -> +% {reply, #response{status = ?STATUS_ERR, error_code = 1036, info = <<"Table ",Table/binary," is readonly">>}, State}; +% true -> +% Values = [{K,V} || #set{key = K, value = #value{value = V}} <- ValuesSpec], +% case Handler:insert(Table, Values) of +% {error, Code, Desc} -> +% {reply, #response{status=?STATUS_ERR, error_code=Code, info=Desc}, State}; +% {ok, Id} when is_integer(Id) -> +% {reply, #response{status=?STATUS_OK, affected_rows = 1, last_insert_id = Id, status_flags = 0, warnings = 0, info = <<>>}, State} +% end +% end; + +% run_query(#request{info = #insert{}}, #my{} = State) -> +% {reply, #response{status=?STATUS_ERR, error_code=1227, info= <<"Admin role required for inserting">>}, State}; + + +% run_query(#request{info = #update{table = #table{name = Table}, set = ValuesSpec, conditions = Conditions}}, #my{handler = Handler, role = admin} = State) -> +% case erlang:function_exported(Handler, update, 3) of +% false -> +% {reply, #response{status = ?STATUS_ERR, error_code = 1036, info = <<"Table ",Table/binary," is readonly">>}, State}; +% true -> +% Values = [{K,V} || #set{key = K, value = #value{value = V}} <- ValuesSpec], +% case Handler:update(Table, Values, Conditions) of +% {error, Code, Desc} -> +% {reply, #response{status=?STATUS_ERR, error_code=Code, info=Desc}, State}; +% {ok, Id} when is_integer(Id) -> +% {reply, #response{status=?STATUS_OK, affected_rows = 1, last_insert_id = Id, status_flags = 0, warnings = 0, info = <<>>}, State} +% end +% end; + +% run_query(#request{info = #update{}}, #my{} = State) -> +% {reply, #response{status=?STATUS_ERR, error_code=1227, info= <<"Admin role required for updating">>}, State}; + + +% run_query(#request{info = #delete{table = #table{name = Table}, conditions = Conditions}}, #my{handler = Handler, role = admin} = State) -> +% case erlang:function_exported(Handler, delete, 2) of +% false -> +% {reply, #response{status = ?STATUS_ERR, error_code = 1036, info = <<"Table ",Table/binary," is readonly">>}, State}; +% true -> +% case Handler:delete(Table, Conditions) of +% {error, Code, Desc} -> +% {reply, #response{status=?STATUS_ERR, error_code=Code, info=Desc}, State}; +% {ok, Id} when is_integer(Id) -> +% {reply, #response{status=?STATUS_OK, affected_rows = 1, last_insert_id = Id, status_flags = 0, warnings = 0, info = <<>>}, State} +% end +% end; + +% run_query(#request{info = #delete{}}, #my{} = State) -> +% {reply, #response{status=?STATUS_ERR, error_code=1227, info= <<"Admin role required for deleting">>}, State}; + + +% run_query(#request{}, #my{} = State) -> +% {reply, #response{status = ?STATUS_ERR, error_code = 1065, info = <<"Invalid select query">>}, State}. + + + + + +% normalize_condition_types(Columns, #condition{nexo = OrAnd, op1 = Op1, op2 = Op2}) when OrAnd == nexo_and; OrAnd == nexo_or -> +% #condition{nexo = OrAnd, +% op1 = normalize_condition_types(Columns, Op1), +% op2 = normalize_condition_types(Columns, Op2) +% }; + +% normalize_condition_types(Columns, #condition{nexo = C, op1 = #key{name = Name} = K, op2 = #value{value = V}} = Cond) when V == 1; V == 0-> +% Column = binary_to_existing_atom(Name,latin1), +% case proplists:get_value(Column, Columns) of +% boolean when V == 1 -> #condition{nexo = C, op1 = K, op2 = #value{value = true}}; +% boolean when V == 0 -> #condition{nexo = C, op1 = K, op2 = #value{value = false}}; +% _ -> Cond +% end; + +% normalize_condition_types(_Columns, Cond) -> +% Cond. + + + + +response_row(Row, Columns) when is_map(Row) -> + [maps:get(Column, Row, undefined) || Column <- Columns]; + +response_row(Row, Columns) when is_list(Row) -> + [proplists:get_value(Column, Row) || Column <- Columns]. + + +response_columns(Columns) -> + lists:map(fun + ({Name,string}) -> #column{name = atom_to_binary(Name,latin1), type = ?TYPE_VAR_STRING, length = 20, org_name = atom_to_binary(Name,latin1)}; + ({Name,boolean}) -> #column{name = atom_to_binary(Name,latin1), type = ?TYPE_TINY, length = 1, org_name = atom_to_binary(Name,latin1)}; + ({Name,integer}) -> #column{name = atom_to_binary(Name,latin1), type = ?TYPE_LONGLONG, length = 20, org_name = atom_to_binary(Name,latin1)} + end, Columns). + + +terminate(_Reason,_) -> + % lager:info("terminate ~p", [Reason]), + ok. diff --git a/test/update_test.erl b/test/update_test.erl deleted file mode 100644 index 582c2c9..0000000 --- a/test/update_test.erl +++ /dev/null @@ -1,58 +0,0 @@ -%% -*- erlang; utf-8 -*- --module(update_test). --author('bombadil@bosqueviejo.net'). - --compile(export_all). - -% required for eunit to work --include_lib("eunit/include/eunit.hrl"). - --include("sql.hrl"). - -%%==================================================================== -%% Test cases -%%==================================================================== - -update_simple_test() -> - ?assertEqual( - mysql:parse("update mitabla set dato=1"), - #update{ - table=#table{alias = <<"mitabla">>, name = <<"mitabla">>}, - set=[#set{key = <<"dato">>, value=#value{value=1}}] - } - ), - ?assertEqual( - mysql:parse(" Update mitabla SET dato = 1 "), - mysql:parse("UPDATE mitabla SET dato=1") - ), - ok. - -update_multiparams_test() -> - ?assertEqual( - mysql:parse("update mitabla set dato1=1, dato2='bon jovi', dato3='this ain''t a love song'"), - #update{ - table=#table{alias = <<"mitabla">>, name = <<"mitabla">>}, - set=[ - #set{key = <<"dato1">>, value=#value{value = 1}}, - #set{key = <<"dato2">>, value=#value{value = <<"bon jovi">>}}, - #set{key = <<"dato3">>, value=#value{value = <<"this ain't a love song">>}} - ] - } - ), - ok. - -update_where_test() -> - ?assertEqual( - mysql:parse("update mitabla set dato=1 where dato=5"), - #update{ - table=#table{alias = <<"mitabla">>, name = <<"mitabla">>}, - set=[#set{key = <<"dato">>, value=#value{value=1}}], - conditions=#condition{ - nexo=eq, - op1=#key{alias = <<"dato">>, name = <<"dato">>}, - op2=#value{value=5} - } - } - ), - ok. -
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor