PageRenderTime 37ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/src/medici/principe_table.erl

http://github.com/evanmiller/ChicagoBoss
Erlang | 828 lines | 444 code | 79 blank | 305 comment | 4 complexity | f73c153f63a983a244fb0b4fcf4c4248 MD5 | raw file
  1. %%%-------------------------------------------------------------------
  2. %%% File: principe_table.erl
  3. %%% @author Jim McCoy <mccoy@mad-scientist.com>
  4. %%% @copyright Copyright (c) 2009, Jim McCoy. All Rights Reserved.
  5. %%%
  6. %%% @doc
  7. %%% An extension to the principe module that handles tables. See the
  8. %%% principe module docs for a note about Tyrant and server byte-order
  9. %%% issues. When using tyrant in table mode this matters far less than
  10. %%% it does for Tyrant in other modes; in most cases Tyrant will encode
  11. %%% table column values internally as strings. The only place that this
  12. %%% matters is using addint or adddouble in conjunction with a row in
  13. %%% which you manually added a magic "_num" column. For this case you
  14. %%% will need to do a bit of magic on your own to properly encode the
  15. %%% float or int using the put() function. See the @see principe module
  16. %%% for examples (use the "bigendian" property from a stat() call to
  17. %%% figure out what your server expects.)
  18. %%% @end
  19. %%%-------------------------------------------------------------------
  20. -module(principe_table).
  21. -export([connect/0, connect/1, put/3, putkeep/3, putcat/3, update/3, out/2,
  22. get/2, mget/2, vsiz/2, iterinit/1, iternext/1, fwmkeys/3, sync/1, optimize/2,
  23. vanish/1, rnum/1, size/1, stat/1, copy/2, restore/3, addint/3, adddouble/3,
  24. adddouble/4, setmst/3, setindex/3, query_limit/3, query_limit/4, query_add_condition/5,
  25. query_order/4, search/2, genuid/1, searchcount/2, searchout/2]).
  26. -include("principe.hrl").
  27. %%====================================================================
  28. %% The Tokyo Tyrant access functions
  29. %%====================================================================
  30. %% @spec connect() -> {ok, port()} | error()
  31. %%
  32. %% @doc
  33. %% Establish a connection to the tyrant service.
  34. %% @end
  35. connect() ->
  36. connect([]).
  37. %% @spec connect(ConnectProps::proplist()) -> {ok, port()} | error()
  38. %%
  39. %% @doc
  40. %% Establish a connection to the tyrant service using properties in the
  41. %% ConnectProps proplist to determine the hostname, port number and tcp
  42. %% socket options for the connection. Any missing parameters are filled
  43. %% in using the module defaults.
  44. %% @end
  45. connect(ConnectProps) ->
  46. {ok, Socket} = principe:connect(ConnectProps),
  47. % make sure we are connection to a tyrant server in table mode
  48. case proplists:get_value(type, principe:stat(Socket)) of
  49. "table" ->
  50. {ok, Socket};
  51. _ ->
  52. {error, no_table_server}
  53. end.
  54. %%====================================================================
  55. %% Standard tyrant functions (straight pass-through to principe.erl)
  56. %%====================================================================
  57. %% @spec mget(Socket::port(),
  58. %% KeyList::keylist()) -> [{Key::binary(), Value::proplist()}] | error()
  59. %%
  60. %% @doc
  61. %% Get the values for a list of keys. Due to the way that columns are returned
  62. %% via the tyrant protocol a null seperator is used to break
  63. mget(Socket, KeyList) ->
  64. case principe:mget(Socket, KeyList) of
  65. {error, Reason} ->
  66. {error, Reason};
  67. MgetResults ->
  68. lists:keymap(fun(BinaryToSplit) ->
  69. columnize_values(binary_to_list(BinaryToSplit), [], [])
  70. end, 2, MgetResults)
  71. end.
  72. %% @spec columnize_values(ColumnValues::list(),
  73. %% Current::list(),
  74. %% Stack::[binary()]) -> [{ColumnName::binary(), ColumnValue::binary()}]
  75. %%
  76. %% @private
  77. %% Convert a list of bytes (generally one that was converted running binary_to_list
  78. %% on the results returned from tyrant) into a proplist of column names and values.
  79. %% This function (like tyrant) uses a null value as the separator for column names
  80. %% and values. A column name that contains a null will cause this function to choke
  81. %% or return invalid data.
  82. %% @end
  83. columnize_values([], Current, Stack) ->
  84. FinalStack = lists:reverse([list_to_binary(lists:reverse(Current)) | Stack]),
  85. return_column_vals(FinalStack, []);
  86. columnize_values([0 | T], [], Stack) ->
  87. columnize_values(T, [], [<<"">>|Stack]);
  88. columnize_values([0 | T], Current, Stack) ->
  89. columnize_values(T, [], [list_to_binary(lists:reverse(Current)) | Stack]);
  90. columnize_values([H | T], Current, Stack) ->
  91. columnize_values(T, [H | Current], Stack).
  92. %% @spec return_column_vals(ValuesToParse::list(),
  93. %% FinalResult::proplist()) -> proplist()
  94. %%
  95. %% @private Take a list with an even number of elements and make it a proplist
  96. return_column_vals([], Cols) ->
  97. Cols;
  98. return_column_vals([K, V | Tail], Cols) ->
  99. return_column_vals(Tail, [{K, V} | Cols]).
  100. %% @spec addint(Socket::port(),
  101. %% Key::key(),
  102. %% Int::integer()) -> integer() | error()
  103. %%
  104. %% @doc Add an integer value to the _num column of a given a key. The
  105. %% _num column will be created if it does not already exist.
  106. %%
  107. %% NOTE: Something truly wierd about this _num column setup is that tc/tyrant
  108. %% expects the column to be a string() value internally. I am assuming this
  109. %% is because for table databases a null is used as a column separator, if the
  110. %% _num value was stored as an integer then differing server byte order (which TC
  111. %% suffers from) would confuse the server. If you put() an integer() value to
  112. %% the _num column it will get overwritten by an addint() call, but if you write
  113. %% a integer_to_list(integer()) value to the num column via a normal put() call
  114. %% things will work correctly.
  115. %% @end
  116. addint(Socket, Key, Int) ->
  117. principe:addint(Socket, Key, Int).
  118. %% @spec adddouble(Socket::port(),
  119. %% Key::key(),
  120. %% Double::float()) -> {Integral::integer(), Fractional::integer()} | error()
  121. %%
  122. %% @doc Add an float value to the _num column of a given a key. The
  123. %% _num column will be created if it does not already exist.
  124. %% @end
  125. adddouble(Socket, Key, Double) ->
  126. principe:adddouble(Socket, Key, Double).
  127. %% @spec adddouble(Socket::port(),
  128. %% Key::key(),
  129. %% Integral::integer(),
  130. %% Fractional::integer()) -> {Integral::integer(), Fractional::integer()} | error()
  131. %%
  132. %% @doc The raw adddouble function for those who need a bit more control on float adds.
  133. adddouble(Socket, Key, Integral, Fractional) ->
  134. principe:adddouble(Socket, Key, Integral, Fractional).
  135. %% @spec iterinit(Socket::port()) -> ok | error()
  136. %%
  137. %% @doc Start iteration protocol. WARNING: The tyrant iteration protocol has no
  138. %% concurrency controls whatsoever, so if multiple clients try to do iteration
  139. %% they will stomp all over each other!
  140. %% @end
  141. iterinit(Socket) ->
  142. principe:iterinit(Socket).
  143. %% @spec iternext(Socket::port()) -> {Key::binary(), Value::binary()} | error()
  144. %%
  145. %% @doc Get the next key/value pair in the iteration protocol.
  146. iternext(Socket) ->
  147. principe:iternext(Socket).
  148. %% @spec fwmkeys(Socket::port(),
  149. %% Prefix::iolist(),
  150. %% MaxKeys::integer()) -> [binary()]
  151. %%
  152. %% @doc Return a number of keys that match a given prefix.
  153. fwmkeys(Socket, Prefix, MaxKeys) ->
  154. principe:fwmkeys(Socket, Prefix, MaxKeys).
  155. %% @spec vsiz(Socket::port(),
  156. %% Key::key()) -> integer()
  157. %%
  158. %% @doc
  159. %% Get the size of the value for a given key. The value returned for
  160. %% a key will be the total of the column size values, and each column
  161. %% size will be the size of the column name (in bytes), the size of the
  162. %% column value (in bytes), plus one for the internal null seperator
  163. %% between column name and value plus one for the null terminator for
  164. %% the column (i.e. length(ColumnName) + length(ColumnValue) + 2 for each
  165. %% column.)
  166. %% @end
  167. vsiz(Socket, Key) ->
  168. principe:vsiz(Socket, Key).
  169. %% @spec sync(Socket::port()) -> ok | error()
  170. %%
  171. %% @doc Call sync() on the remote database.
  172. sync(Socket) ->
  173. principe:sync(Socket).
  174. %% @spec vanish(Socket::port()) -> ok | error()
  175. %%
  176. %% @doc Remove all records from the remote database.
  177. vanish(Socket) ->
  178. principe:vanish(Socket).
  179. %% @spec optimize(Socket::port(),
  180. %% Params::list()) -> ok | error()
  181. %%
  182. %% @doc Change the remote database tuning parameters
  183. optimize(Socket, Params) ->
  184. principe:optimize(Socket, Params).
  185. %% Get the number of records in the remote database
  186. rnum(Socket) ->
  187. principe:rnum(Socket).
  188. %% @spec size(Socket::port()) -> integer() | error()
  189. %%
  190. %% @doc Get the size in bytes of the remote database.
  191. size(Socket) ->
  192. principe:size(Socket).
  193. %% @spec stat(Socket::port()) -> proplist() | error()
  194. %%
  195. %% @doc Get the status string of a remote database.
  196. stat(Socket) ->
  197. principe:stat(Socket).
  198. %% @spec copy(Socket::port(),
  199. %% iolist()) -> ok | error()
  200. %%
  201. %% @doc Make a copy of the database file of the remote database.
  202. copy(Socket, PathName) ->
  203. principe:copy(Socket, PathName).
  204. %% @spec restore(Socket::port(),
  205. %% PathName::iolist(),
  206. %% TimeStamp::integer) -> ok | error()
  207. %%
  208. %% @doc Restore the database to a particular point in time from the update log.
  209. restore(Socket, PathName, TimeStamp) ->
  210. principe:restore(Socket, PathName, TimeStamp).
  211. %% @spec setmst(Socket::port(),
  212. %% HostName::iolist(),
  213. %% Port::integer) -> ok | error()
  214. %%
  215. %% @doc Set the replication master of a remote database server.
  216. setmst(Socket, HostName, Port) ->
  217. principe:setmst(Socket, HostName, Port).
  218. %%====================================================================
  219. %% Table functions
  220. %%====================================================================
  221. %% @spec put(Socket::port(),
  222. %% Key::key(),
  223. %% Cols::coldata()) -> [] | error()
  224. %%
  225. %% @doc
  226. %% Call the Tyrant server to store a new set of column values for the given key.
  227. %% @end
  228. put(Socket, Key, Cols) ->
  229. Data = encode_table(Cols),
  230. ?TSimple(<<"put">>, [Key | Data]).
  231. %% @spec putkeep(Socket::port(),
  232. %% Key::key(),
  233. %% Cols::coldata()) -> [] | error()
  234. %%
  235. %% @doc
  236. %% Call the Tyrant server to add a set of column values for a given key. Will
  237. %% return an error if Key is already in the remote database.
  238. %% @end
  239. putkeep(Socket, Key, Cols) ->
  240. Data = encode_table(Cols),
  241. ?TSimple(<<"putkeep">>, [Key | Data]).
  242. %% @spec putcat(Socket::port(),
  243. %% Key::key(),
  244. %% Cols::coldata()) -> [] | error()
  245. %%
  246. %% @doc
  247. %% Concatenate a set of column values to the existing value of Key (or
  248. %% create a new entry for Key with the given column values if Key is not
  249. %% in the remote database.) If any columns in Cols already have values
  250. %% for the given key then the entries provided in the Cols parameter for
  251. %% those specific columns will be ignored by the remote database. Use the
  252. %% update() function to overwrite existing column values.
  253. %% @end
  254. putcat(Socket, Key, Cols) ->
  255. Data = encode_table(Cols),
  256. ?TSimple(<<"putcat">>, [Key | Data]).
  257. %% @spec update(Socket::port(),
  258. %% Key::key(),
  259. %% Cols::coldata()) -> [] | error()
  260. %%
  261. %% @doc
  262. %% Update a table entry by merging Cols into existing data for given key. The
  263. %% end result of this function should be to create a new entry for Key whose
  264. %% column values are the new data from the Cols parameter as well as any previous
  265. %% columns for Key that were not in the Cols proplist.
  266. %% @end
  267. %%
  268. %% TODO: better way would be to use a lua server script to perform the merge?
  269. update(Socket, Key, Cols) ->
  270. case principe:misc(Socket, <<"get">>, [Key]) of
  271. {error, _Reason} ->
  272. UpdatedProps = Cols;
  273. ExistingData ->
  274. OldProps = decode_table(ExistingData),
  275. NewProps = lists:foldl(fun({K, V}, AccIn) when is_list(K) ->
  276. [{list_to_binary(K), V} | AccIn];
  277. ({K, V}, AccIn) when is_atom(K) ->
  278. [{list_to_binary(atom_to_list(K)), V} | AccIn];
  279. (Other, AccIn) -> [Other | AccIn]
  280. end, OldProps, Cols),
  281. UpdatedProps = [{K, proplists:get_value(K, NewProps)} || K <- proplists:get_keys(NewProps)]
  282. end,
  283. Data = encode_table(UpdatedProps),
  284. ?TSimple(<<"put">>, [Key | Data]).
  285. %% @spec out(Socket::port(),
  286. %% Key::key()) -> ok | error()
  287. %%
  288. %% @doc
  289. %% Remove a key from the remote database. Will return an error if Key is
  290. %% not in the database.
  291. %% @end
  292. out(Socket, Key) ->
  293. ?TSimple(<<"out">>, [Key]).
  294. %% @spec get(Socket::port(),
  295. %% Key::key()) -> proplist() | error()
  296. %%
  297. %% @doc Get the value for a given key. Table data is returned in a proplist of
  298. %% {ColumnName, ColumnValue} tuples.
  299. %% @end
  300. get(Socket, Key) ->
  301. case ?TRaw(<<"get">>, [Key]) of
  302. {error, Reason} ->
  303. {error, Reason};
  304. RecList ->
  305. decode_table(RecList)
  306. end.
  307. %% @spec setindex(Socket::port(),
  308. %% ColName::index_col(),
  309. %% Type::index_type()) -> ok | error()
  310. %%
  311. %% @doc
  312. %% Tell the tyrant server to build an index for a column. The ColName
  313. %% should be either the atom "primary" (to index on the primary key) or a
  314. %% iolist() that names the column to be indexed. Type should be an atom
  315. %% selected from decimal (index column as decimal data), lexical (index as
  316. %% character/string data) or void (remove an existing index for ColName).
  317. %% @end
  318. setindex(Socket, primary, Type) when is_atom(Type) ->
  319. ?TSimple(<<"setindex">>, [?NULL, setindex_request_val(Type)]);
  320. setindex(Socket, ColName, Type) when is_atom(Type) ->
  321. ?TSimple(<<"setindex">>, [ColName, setindex_request_val(Type)]).
  322. %% @spec genuid(Socket::port()) -> binary() | error()
  323. %%
  324. %% @doc Generate a unique id within the set of primary keys
  325. genuid(Socket) ->
  326. case ?TRaw(<<"genuid">>, []) of
  327. [NewId] ->
  328. NewId;
  329. Error ->
  330. Error
  331. end.
  332. %% @spec query_add_condition(Query::proplist(),
  333. %% ColName::iolist(),
  334. %% Op::query_opcode(),
  335. %% ExprList::query_expr()) -> proplist()
  336. %%
  337. %% @doc
  338. %% Add a condition for a query. ExprList should be a list of one or more
  339. %% values where each value is either a binary, string, or integer. Op can be
  340. %% either an atom or a tuple of atoms describing the operation. If the first
  341. %% atom in an Op tuple is "no" then the condition is a negation query and if
  342. %% the last atom is no_index an existing index on the remote database server will
  343. %% be bypassed.
  344. %% @end
  345. query_add_condition(_Sock, Query, ColName, Op, ExprList) when is_list(ExprList) ->
  346. [{{add_cond, ColName, Op, ExprList},
  347. ["addcond",
  348. ?NULL,
  349. ColName,
  350. ?NULL,
  351. integer_to_list(add_condition_op_val(Op)),
  352. ?NULL,
  353. convert_query_exprlist(ExprList)]
  354. } | Query].
  355. %% @spec query_limit(Query::proplist(),
  356. %% Max::integer(),
  357. %% Skip::integer()) -> proplist()
  358. %%
  359. %% @doc Set a limit on the number of returned values for Query, skip the first Skip records.
  360. query_limit(_Sock, Query, Max, Skip) when is_integer(Max), Max > 0, is_integer(Skip), Skip >= 0 ->
  361. LimitKey = {set_limit, Max, Skip},
  362. LimitValue = ["setlimit",
  363. ?NULL,
  364. integer_to_list(Max),
  365. ?NULL,
  366. integer_to_list(Skip)],
  367. case lists:keysearch(set_limit, 1, proplists:get_keys(Query)) of
  368. false ->
  369. [{LimitKey, LimitValue} | Query];
  370. {value, ExistingKey} ->
  371. [{LimitKey, LimitValue} | proplists:delete(ExistingKey, Query)]
  372. end.
  373. %% @spec query_limit(Query::proplist(),
  374. %% Max::integer()) -> proplist()
  375. %%
  376. %% @doc Set a limit on the number of returned values for Query.
  377. %%
  378. %% XXX: should the missing skip be 0 or -1 (protocol ref and perl versions seem to disagree)
  379. query_limit(_Sock, Query, Max) ->
  380. query_limit(_Sock, Query, Max, 0).
  381. %% @spec query_order(Query::proplist(),
  382. %% ColName::index_col(),
  383. %% Type::order_type()) -> proplist()
  384. %%
  385. %% @doc Set the order for returned values in Query.
  386. query_order(_Sock, Query, primary, Type) when is_atom(Type) ->
  387. OrderKey = {set_order, primary, Type},
  388. OrderValue = ["setorder",
  389. ?NULL,
  390. "",
  391. ?NULL,
  392. integer_to_list(order_request_val(Type))],
  393. case lists:keysearch(set_order, 1, proplists:get_keys(Query)) of
  394. false ->
  395. [{OrderKey, OrderValue} | Query];
  396. {value, ExistingKey} ->
  397. [{OrderKey, OrderValue} | proplists:delete(ExistingKey, Query)]
  398. end;
  399. query_order(_Sock, Query, ColName, Type) when is_atom(Type) ->
  400. OrderKey = {set_order, ColName, Type},
  401. OrderValue = ["setorder",
  402. ?NULL,
  403. ColName,
  404. ?NULL,
  405. integer_to_list(order_request_val(Type))],
  406. case lists:keysearch(set_order, 1, proplists:get_keys(Query)) of
  407. false ->
  408. [{OrderKey, OrderValue} | Query];
  409. {value, ExistingKey} ->
  410. [{OrderKey, OrderValue} | proplists:delete(ExistingKey, Query)]
  411. end.
  412. %% @spec search(Socket::port,
  413. %% Query::proplist()) -> keylist() | error()
  414. %%
  415. %% @doc Run a prepared query against the table and return matching keys.
  416. search(Socket, Query) ->
  417. ?TRaw(<<"search">>, [V || {_K, V}=Prop <- Query, is_tuple(Prop), erlang:size(Prop)==2]).
  418. %% @spec searchcount(Socket::port,
  419. %% Query::proplist()) -> [integer()] | error()
  420. %%
  421. %% @doc Run a prepared query against the table and get the count of matching keys.
  422. searchcount(Socket, Query) ->
  423. case ?TRaw(<<"search">>, [V || {_K, V}=Prop <- Query, is_tuple(Prop), erlang:size(Prop)==2] ++ ["count"]) of
  424. {error, Reason} ->
  425. {error, Reason};
  426. [] ->
  427. 0;
  428. [Count] ->
  429. list_to_integer(binary_to_list(Count))
  430. end.
  431. %% @spec searchout(Socket::port,
  432. %% Query::proplist()) -> ok | error()
  433. %%
  434. %% @doc Run a prepared query against the table and remove the matching records.
  435. searchout(Socket, Query) ->
  436. ?TSimple(<<"search">>, [V || {_K, V}=Prop <- Query, is_tuple(Prop), erlang:size(Prop)==2] ++ ["out"]).
  437. %% %% Run a prepared query against the table and get the matching records. Due
  438. %% %% to protocol restraints, the returned result cannot include columns whose
  439. %% %% name or value include the null (0x0) character.
  440. %% tblsearchget(Socket, TblQuery) ->
  441. %% void.
  442. %% tblrescols(Socket, TblQuery) ->
  443. %% void.
  444. %%====================================================================
  445. %% Table utility functions
  446. %%====================================================================
  447. %% @spec add_condition_op_val(query_op()) -> integer()
  448. %%
  449. %% @private Decode add_contition operation tag
  450. add_condition_op_val({no, Op}) when is_atom(Op) ->
  451. ?QCNEGATE bor add_condition_op_val(Op);
  452. add_condition_op_val({Op, no_index}) when is_atom(Op) ->
  453. ?QCNOIDX bor add_condition_op_val(Op);
  454. add_condition_op_val({no, Op, no_index}) when is_atom(Op)->
  455. ?QCNEGATE bor ?QCNOIDX bor add_condition_op_val(Op);
  456. add_condition_op_val({Op}) when is_atom(Op) ->
  457. add_condition_op_val(Op);
  458. add_condition_op_val(Op) when is_atom(Op) ->
  459. case Op of
  460. str_eq ->
  461. ?QCSTREQ;
  462. str_inc ->
  463. ?QCSTRINC;
  464. str_begin ->
  465. ?QCSTRBW;
  466. str_end ->
  467. ?QCSTREW;
  468. str_and ->
  469. ?QCSTRAND;
  470. str_or ->
  471. ?QCSTROR;
  472. str_in_list ->
  473. ?QCSTROREQ;
  474. str_regex ->
  475. ?QCSTRRX;
  476. num_eq ->
  477. ?QCNUMEQ;
  478. num_gt ->
  479. ?QCNUMGT;
  480. num_ge ->
  481. ?QCNUMGE;
  482. num_lt ->
  483. ?QCNUMLT;
  484. num_le ->
  485. ?QCNUMLE;
  486. num_between ->
  487. ?QCNUMBT;
  488. num_in_list ->
  489. ?QCNUMOREQ
  490. end.
  491. %% @spec setindex_request_val(index_type()) -> integer()
  492. %%
  493. %% @private Decode set_index request tag
  494. setindex_request_val(Type) ->
  495. case Type of
  496. lexical ->
  497. ?ITLEXICAL;
  498. decimal ->
  499. ?ITDECIMAL;
  500. optimized ->
  501. ?ITOPT;
  502. void ->
  503. ?ITVOID
  504. end.
  505. %% @spec order_request_val(order_type()) -> integer()
  506. %%
  507. %% @private Decode result order tag
  508. order_request_val(Type) ->
  509. case Type of
  510. str_ascending ->
  511. ?QOSTRASC;
  512. str_descending ->
  513. ?QOSTRDESC;
  514. num_ascending ->
  515. ?QONUMASC;
  516. num_descending ->
  517. ?QONUMDESC
  518. end.
  519. %% @spec convert_query_exprlist(query_expr()) -> [string()]
  520. %%
  521. %% @private
  522. %% Convert query expression list to comma-seperated list of string values.
  523. convert_query_exprlist(ExprList) ->
  524. convert_query_exprlist(ExprList, []).
  525. convert_query_exprlist([H | T], []) when is_integer(H) ->
  526. convert_query_exprlist(T, [integer_to_list(H)]);
  527. convert_query_exprlist([H | T], []) when is_binary(H) ->
  528. convert_query_exprlist(T, [binary_to_list(H)]);
  529. convert_query_exprlist([H | T], []) ->
  530. convert_query_exprlist(T, [H]);
  531. convert_query_exprlist([H | T], Acc) when is_integer(H) ->
  532. convert_query_exprlist(T, [integer_to_list(H) | ["," | Acc]]);
  533. convert_query_exprlist([H | T], Acc) when is_binary(H) ->
  534. convert_query_exprlist(T, [binary_to_list(H) | ["," | Acc]]);
  535. convert_query_exprlist([H | T], Acc) ->
  536. convert_query_exprlist(T, [H | ["," | Acc]]);
  537. convert_query_exprlist([], Acc) ->
  538. lists:reverse(Acc).
  539. %% @spec encode_table(proplist()) -> [value_or_num()]
  540. %%
  541. %% @private Convert proplist to a list of key, value sequences.
  542. encode_table(Data) when is_list(Data) ->
  543. encode_table(Data, []).
  544. encode_table([], Acc) ->
  545. lists:reverse(Acc);
  546. encode_table([{K, V} | Tail], Acc) ->
  547. encode_table(Tail, [V | [ K | Acc]]).
  548. %% @spec decode_table([value_or_num()]) -> proplist() | error()
  549. %%
  550. %% @private Convert list of key, value pairs to a proplist
  551. decode_table({error, Code}) ->
  552. {error, Code};
  553. decode_table(Data) when is_list(Data) ->
  554. decode_table(Data, []).
  555. decode_table([], Acc) ->
  556. lists:reverse(Acc);
  557. decode_table([K, V | Tail], Acc) ->
  558. decode_table(Tail, [{K, V} | Acc]).
  559. %% Some standard types for edoc
  560. %%
  561. %% @type endian() = big | little
  562. %% @type key() = iolist()
  563. %% @type value() = iolist()
  564. %% @type value_or_num() = iolist() | integer() | float()
  565. %% @type keylist() = [key()]
  566. %% @type coldata() = [{key(), value_or_num()}]
  567. %% @type error() = {error, term()}
  568. %% @type index_col() = primary | iolist()
  569. %% @type index_type() = lexical | decimal | void
  570. %% @type query_opcode() = atom() | tuple()
  571. %% @type query_expr() = [binary() | string() | integer()]
  572. %% @type order_type() = str_ascending | str_descending | num_ascending | num_descending
  573. %% EUnit tests
  574. %%
  575. -ifdef(EUNIT).
  576. test_setup() ->
  577. {ok, Socket} = ?MODULE:connect(),
  578. ok = ?MODULE:vanish(Socket),
  579. Socket.
  580. test_setup_with_data() ->
  581. Socket = test_setup(),
  582. ColData = [{"rec1", [{"name", "alice"}, {"sport", "baseball"}]},
  583. {"rec2", [{"name", "bob"}, {"sport", "basketball"}]},
  584. {"rec3", [{"name", "carol"}, {"age", "24"}]},
  585. {"rec4", [{"name", "trent"}, {"age", "33"}, {"sport", "football"}]},
  586. {"rec5", [{"name", "mallet"}, {"sport", "tennis"}, {"fruit", "apple"}]}
  587. ],
  588. lists:foreach(fun({Key, ValProplist}) ->
  589. ok = ?MODULE:put(Socket, Key, ValProplist)
  590. end, ColData),
  591. Socket.
  592. put_get_test() ->
  593. Socket = test_setup_with_data(),
  594. ?assertMatch([{<<"age">>, <<"24">>}, {<<"name">>, <<"carol">>}], lists:sort(?MODULE:get(Socket, "rec3"))),
  595. ok = ?MODULE:put(Socket, <<"put_get1">>, [{"num", 32}]),
  596. % Note that by default integers go over to Tyrant in network byte-order
  597. ?assertMatch([{<<"num">>, <<32:32>>}], lists:sort(?MODULE:get(Socket, <<"put_get1">>))),
  598. ok.
  599. putkeep_test() ->
  600. Socket = test_setup(),
  601. ok = ?MODULE:put(Socket, "putkeep1", [{"col1", "testval1"}]),
  602. ?assertMatch([{<<"col1">>, <<"testval1">>}], ?MODULE:get(Socket, "putkeep1")),
  603. ?assertMatch({error, _}, ?MODULE:putkeep(Socket, <<"putkeep1">>, [{"col1", "testval2"}])),
  604. ?assertMatch([{<<"col1">>, <<"testval1">>}], ?MODULE:get(Socket, "putkeep1")),
  605. ok = ?MODULE:putkeep(Socket, <<"putkeep2">>, [{"col1", "testval2"}]),
  606. ?assertMatch([{<<"col1">>, <<"testval2">>}], ?MODULE:get(Socket, "putkeep2")),
  607. ok.
  608. putcat_test() ->
  609. Socket = test_setup_with_data(),
  610. ?assertMatch([{<<"age">>, <<"24">>}, {<<"name">>, <<"carol">>}],
  611. lists:sort(?MODULE:get(Socket, "rec3"))),
  612. ok = ?MODULE:putcat(Socket, "rec3", [{"sport", "golf"}]),
  613. ?assertMatch([{<<"age">>, <<"24">>}, {<<"name">>, <<"carol">>}, {<<"sport">>, <<"golf">>}],
  614. lists:sort(?MODULE:get(Socket, "rec3"))),
  615. ok.
  616. update_test() ->
  617. Socket = test_setup_with_data(),
  618. ?assertMatch([{<<"name">>, <<"alice">>}, {<<"sport">>, <<"baseball">>}], ?MODULE:get(Socket, "rec1")),
  619. ok = ?MODULE:update(Socket, "rec1", [{"sport", "swimming"}, {"pet", "dog"}]),
  620. ?assertMatch([{<<"name">>, <<"alice">>}, {<<"pet">>, <<"dog">>}, {<<"sport">>, <<"swimming">>}],
  621. lists:sort(?MODULE:get(Socket, "rec1"))),
  622. ok.
  623. out_test() ->
  624. Socket = test_setup_with_data(),
  625. ok = ?MODULE:out(Socket, <<"rec1">>),
  626. ?assertMatch({error, _}, ?MODULE:get(Socket, <<"rec1">>)),
  627. ok.
  628. vsiz_test() ->
  629. Socket = test_setup(),
  630. ColName = "col1",
  631. ColVal = "vsiz test",
  632. ok = ?MODULE:put(Socket, "vsiz1", [{ColName, ColVal}]),
  633. %% size = col + null sep + val + null column stop
  634. ExpectedLength = length(ColName) + length(ColVal) + 2,
  635. ?assert(?MODULE:vsiz(Socket, "vsiz1") =:= ExpectedLength),
  636. ColName2 = "another col",
  637. ColVal2 = "more bytes",
  638. ok = ?MODULE:put(Socket, "vsiz2", [{ColName, ColVal}, {ColName2, ColVal2}]),
  639. ExpectedLength2 = ExpectedLength + length(ColName2) + length(ColVal2) + 2,
  640. ?assert(?MODULE:vsiz(Socket, "vsiz2") =:= ExpectedLength2),
  641. ok.
  642. vanish_test() ->
  643. Socket = test_setup(),
  644. ok = ?MODULE:put(Socket, "vanish1", [{"col1", "going away"}]),
  645. ok = ?MODULE:vanish(Socket),
  646. ?assertMatch({error, _}, ?MODULE:get(Socket, "vanish1")),
  647. ok.
  648. addint_test() ->
  649. Socket = test_setup(),
  650. ?assert(?MODULE:addint(Socket, "addint1", 100) =:= 100),
  651. ok = ?MODULE:put(Socket, "addint2", [{"_num", "10"}]), % see ?MODULE:addint edoc for why a string() is used
  652. ?assert(?MODULE:addint(Socket, "addint2", 10) =:= 20),
  653. ?assertMatch([{<<"_num">>, <<"100">>}], ?MODULE:get(Socket, "addint1")),
  654. ?assertMatch([{<<"_num">>, <<"20">>}], ?MODULE:get(Socket, "addint2")),
  655. ok.
  656. sync_test() ->
  657. Socket = test_setup(),
  658. ok = ?MODULE:sync(Socket),
  659. ok.
  660. rnum_test() ->
  661. Socket = test_setup_with_data(),
  662. ?assert(?MODULE:rnum(Socket) =:= 5),
  663. ok = ?MODULE:out(Socket, "rec1"),
  664. ?assert(?MODULE:rnum(Socket) =:= 4),
  665. ok = ?MODULE:vanish(Socket),
  666. ?assert(?MODULE:rnum(Socket) =:= 0),
  667. ok.
  668. size_test() ->
  669. Socket = test_setup(),
  670. ?MODULE:size(Socket),
  671. ok.
  672. stat_test() ->
  673. Socket = test_setup(),
  674. ?MODULE:stat(Socket),
  675. ok.
  676. mget_test() ->
  677. Socket = test_setup_with_data(),
  678. MGetData = ?MODULE:mget(Socket, ["rec1", "rec3", "rec5"]),
  679. ?assertMatch([{<<"name">>, <<"alice">>},{<<"sport">>, <<"baseball">>}],
  680. lists:sort(proplists:get_value(<<"rec1">>, MGetData))),
  681. ?assert(proplists:get_value(<<"rec2">>, MGetData) =:= undefined),
  682. ?assertMatch([<<"rec1">>, <<"rec3">>, <<"rec5">>], lists:sort(proplists:get_keys(MGetData))),
  683. ok.
  684. iter_test() ->
  685. Socket = test_setup_with_data(),
  686. AllKeys = [<<"rec1">>, <<"rec2">>, <<"rec3">>, <<"rec4">>, <<"rec5">>],
  687. ok = ?MODULE:iterinit(Socket),
  688. First = ?MODULE:iternext(Socket),
  689. ?assert(lists:member(First, AllKeys)),
  690. IterAll = lists:foldl(fun(_Count, Acc) -> [?MODULE:iternext(Socket) | Acc] end,
  691. [First],
  692. lists:seq(1, length(AllKeys)-1)),
  693. ?assertMatch(AllKeys, lists:sort(IterAll)),
  694. ?assertMatch({error, _}, ?MODULE:iternext(Socket)),
  695. ok.
  696. fwmkeys_test() ->
  697. Socket = test_setup_with_data(),
  698. ok = ?MODULE:put(Socket, "fwmkeys1", [{"foo", "bar"}]),
  699. ?assert(length(?MODULE:fwmkeys(Socket, "rec", 4)) =:= 4),
  700. ?assert(length(?MODULE:fwmkeys(Socket, "rec", 8)) =:= 5),
  701. ?assertMatch([<<"fwmkeys1">>], ?MODULE:fwmkeys(Socket, "fwm", 3)),
  702. ?assertMatch([<<"rec1">>, <<"rec2">>, <<"rec3">>], ?MODULE:fwmkeys(Socket, "rec", 3)),
  703. ok.
  704. query_generation_test() ->
  705. ?assertMatch([{{set_order, primary, str_descending}, ["setorder", <<0:8>>, "", <<0:8>>, "1"]}],
  706. ?MODULE:query_order([], primary, str_descending)),
  707. ?assertMatch([{{set_order, "foo", str_ascending}, ["setorder", <<0:8>>, "foo", <<0:8>>, "0"]}],
  708. ?MODULE:query_order([{{set_order, blah}, ["foo"]}], "foo", str_ascending)),
  709. ?assertMatch([{{set_limit, 2, 0}, ["setlimit", <<0:8>>, "2", <<0:8>>, "0"]}],
  710. ?MODULE:query_limit([], 2)),
  711. ?assertMatch([{{set_limit, 4, 1}, ["setlimit", <<0:8>>, "4", <<0:8>>, "1"]}],
  712. ?MODULE:query_limit([{{set_limit, blah}, ["foo"]}], 4, 1)),
  713. ?assertMatch([{{add_cond, "foo", str_eq, ["bar"]}, ["addcond", <<0:8>>, "foo", <<0:8>>, "0", <<0:8>>, ["bar"]]}],
  714. ?MODULE:query_condition([], "foo", str_eq, ["bar"])),
  715. ?assertMatch([{{add_cond, "foo", {no, str_and}, ["bar","baz"]},
  716. ["addcond", <<0:8>>, "foo", <<0:8>>, "16777220", <<0:8>>, ["bar",",","baz"]]}],
  717. ?MODULE:query_condition([], "foo", {no, str_and}, ["bar", "baz"])),
  718. ok.
  719. search_test() ->
  720. Socket = test_setup_with_data(),
  721. Query1 = ?MODULE:query_condition([], "name", str_eq, ["alice"]),
  722. ?assertMatch([<<"rec1">>], ?MODULE:search(Socket, Query1)),
  723. Query2 = ?MODULE:query_condition([], "name", {no, str_eq}, ["alice"]),
  724. Query2A = ?MODULE:query_limit(Query2, 2),
  725. ?assertMatch([<<"rec2">>, <<"rec3">>], ?MODULE:search(Socket, Query2A)),
  726. Query3 = ?MODULE:query_condition([], "age", num_ge, [25]),
  727. ?assertMatch([<<"rec4">>], ?MODULE:search(Socket, Query3)),
  728. Query4 = ?MODULE:query_condition([], "name", {no, str_eq}, ["alice"]),
  729. Query4A = ?MODULE:query_order(Query4, "name", str_descending),
  730. ?assertMatch([<<"rec4">>, <<"rec5">>, <<"rec3">>, <<"rec2">>], ?MODULE:search(Socket, Query4A)),
  731. Query5 = ?MODULE:query_order([], primary, str_descending),
  732. ?assertMatch([<<"rec5">>, <<"rec4">>, <<"rec3">>, <<"rec2">>, <<"rec1">>], ?MODULE:search(Socket, Query5)),
  733. ok.
  734. searchcount_test() ->
  735. Socket = test_setup_with_data(),
  736. Query1 = ?MODULE:query_condition([], "name", str_or, ["alice", "bob"]),
  737. ?assert(?MODULE:searchcount(Socket, Query1) =:= 2),
  738. ok.
  739. searchout_test() ->
  740. Socket = test_setup_with_data(),
  741. ?assert(?MODULE:rnum(Socket) =:= 5),
  742. %% Also testing regex matches, should hit "baseball" and "basketball" but
  743. %% skip "football"
  744. Query1 = ?MODULE:query_condition([], "sport", str_regex, ["^ba"]),
  745. ok = ?MODULE:searchout(Socket, Query1),
  746. ?assert(?MODULE:rnum(Socket) =:= 3),
  747. ?assertMatch({error, _}, ?MODULE:get(Socket, "rec1")),
  748. ok.
  749. -endif.