/src/dbdrivers/postgresql/z_db.erl

https://code.google.com/p/zotonic/ · Erlang · 654 lines · 498 code · 94 blank · 62 comment · 13 complexity · d046927e814245136361421e01bc07d8 MD5 · raw file

  1. %% @author Marc Worrell <marc@worrell.nl>
  2. %% @copyright 2009 Marc Worrell
  3. %% Date: 2009-04-07
  4. %%
  5. %% @doc Interface to database, uses database definition from Context
  6. %% Copyright 2009 Marc Worrell
  7. %%
  8. %% Licensed under the Apache License, Version 2.0 (the "License");
  9. %% you may not use this file except in compliance with the License.
  10. %% You may obtain a copy of the License at
  11. %%
  12. %% http://www.apache.org/licenses/LICENSE-2.0
  13. %%
  14. %% Unless required by applicable law or agreed to in writing, software
  15. %% distributed under the License is distributed on an "AS IS" BASIS,
  16. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. %% See the License for the specific language governing permissions and
  18. %% limitations under the License.
  19. -module(z_db).
  20. -author("Marc Worrell <marc@worrell.nl").
  21. %% interface functions
  22. -export([
  23. has_connection/1,
  24. transaction/2,
  25. transaction_clear/1,
  26. set/3,
  27. get/2,
  28. get_parameter/2,
  29. assoc_row/2,
  30. assoc_row/3,
  31. assoc_props_row/2,
  32. assoc_props_row/3,
  33. assoc/2,
  34. assoc/3,
  35. assoc_props/2,
  36. assoc_props/3,
  37. q/2,
  38. q/3,
  39. q1/2,
  40. q1/3,
  41. q_row/2,
  42. q_row/3,
  43. equery/2,
  44. equery/3,
  45. insert/2,
  46. insert/3,
  47. update/4,
  48. delete/3,
  49. select/3,
  50. columns/2,
  51. column_names/2,
  52. update_sequence/3,
  53. table_exists/2,
  54. ensure_table/3,
  55. drop_table/2,
  56. flush/1,
  57. assert_table_name/1,
  58. prepare_cols/2
  59. ]).
  60. -include_lib("pgsql.hrl").
  61. -include_lib("zotonic.hrl").
  62. %% @doc Perform a function inside a transaction, do a rollback on exceptions
  63. %% @spec transaction(Function, Context) -> FunctionResult | {error, Reason}
  64. transaction(Function, #context{dbc=undefined} = Context) ->
  65. case has_connection(Context) of
  66. true ->
  67. Host = Context#context.host,
  68. {ok, C} = pgsql_pool:get_connection(Host),
  69. Context1 = Context#context{dbc=C},
  70. Result = try
  71. {ok, [], []} = pgsql:squery(C, "BEGIN"),
  72. R = Function(Context1),
  73. {ok, [], []} = pgsql:squery(C, "COMMIT"),
  74. R
  75. catch
  76. _:Why ->
  77. pgsql:squery(C, "ROLLBACK"),
  78. {rollback, {Why, erlang:get_stacktrace()}}
  79. end,
  80. pgsql_pool:return_connection(Host, C),
  81. Result;
  82. false ->
  83. {rollback, {no_database_connection, erlang:get_stacktrace()}}
  84. end;
  85. transaction(Function, Context) ->
  86. % Nested transaction, only keep the outermost transaction
  87. Function(Context).
  88. %% @doc Clear any transaction in the context, useful when starting a thread with this context.
  89. transaction_clear(#context{dbc=undefined} = Context) ->
  90. Context;
  91. transaction_clear(Context) ->
  92. Context#context{dbc=undefined}.
  93. %% @doc Simple get/set functions for db property lists
  94. set(Key, Props, Value) ->
  95. lists:keystore(Key, 1, Props, {Key, Value}).
  96. get(Key, Props) ->
  97. proplists:get_value(Key, Props).
  98. %% @doc Check if we have database connection
  99. has_connection(#context{host=Host}) ->
  100. is_pid(erlang:whereis(Host)).
  101. %% @doc Transaction handler safe function for fetching a db connection
  102. get_connection(#context{dbc=undefined, host=Host} = Context) ->
  103. case has_connection(Context) of
  104. true ->
  105. {ok, C} = pgsql_pool:get_connection(Host),
  106. C;
  107. false ->
  108. none
  109. end;
  110. get_connection(Context) ->
  111. Context#context.dbc.
  112. %% @doc Transaction handler safe function for releasing a db connection
  113. return_connection(C, #context{dbc=undefined, host=Host}) ->
  114. pgsql_pool:return_connection(Host, C);
  115. return_connection(_C, _Context) ->
  116. ok.
  117. assoc_row(Sql, Context) ->
  118. assoc_row(Sql, [], Context).
  119. assoc_row(Sql, Parameters, Context) ->
  120. case assoc(Sql, Parameters, Context) of
  121. [Row|_] -> Row;
  122. [] -> undefined
  123. end.
  124. assoc_props_row(Sql, Context) ->
  125. assoc_props_row(Sql, [], Context).
  126. assoc_props_row(Sql, Parameters, Context) ->
  127. case assoc_props(Sql, Parameters, Context) of
  128. [Row|_] -> Row;
  129. [] -> undefined
  130. end.
  131. get_parameter(Parameter, Context) ->
  132. C = get_connection(Context),
  133. try
  134. {ok, Result} = pgsql:get_parameter(C, z_convert:to_binary(Parameter)),
  135. Result
  136. after
  137. return_connection(C, Context)
  138. end.
  139. %% @doc Return property lists of the results of a query on the database in the Context
  140. %% @spec assoc(SqlQuery, Context) -> Rows
  141. assoc(Sql, Context) ->
  142. assoc(Sql, [], Context).
  143. assoc(Sql, Parameters, Context) ->
  144. case get_connection(Context) of
  145. none -> [];
  146. C ->
  147. try
  148. {ok, Result} = pgsql:assoc(C, Sql, Parameters),
  149. Result
  150. after
  151. return_connection(C, Context)
  152. end
  153. end.
  154. assoc_props(Sql, Context) ->
  155. assoc_props(Sql, [], Context).
  156. assoc_props(Sql, Parameters, Context) ->
  157. case get_connection(Context) of
  158. none -> [];
  159. C ->
  160. try
  161. {ok, Result} = pgsql:assoc(C, Sql, Parameters),
  162. merge_props(Result)
  163. after
  164. return_connection(C, Context)
  165. end
  166. end.
  167. q(Sql, Context) ->
  168. q(Sql, [], Context).
  169. q(Sql, Parameters, Context) ->
  170. case get_connection(Context) of
  171. none -> [];
  172. C ->
  173. try
  174. case pgsql:equery(C, Sql, Parameters) of
  175. {ok, _Affected, _Cols, Rows} -> Rows;
  176. {ok, _Cols, Rows} -> Rows;
  177. {ok, Rows} -> Rows
  178. end
  179. after
  180. return_connection(C, Context)
  181. end
  182. end.
  183. q1(Sql, Context) ->
  184. q1(Sql, [], Context).
  185. q1(Sql, Parameters, Context) ->
  186. case get_connection(Context) of
  187. none -> undefined;
  188. C ->
  189. try
  190. case pgsql:equery1(C, Sql, Parameters) of
  191. {ok, Value} -> Value;
  192. {error, noresult} -> undefined
  193. end
  194. after
  195. return_connection(C, Context)
  196. end
  197. end.
  198. q_row(Sql, Context) ->
  199. q_row(Sql, [], Context).
  200. q_row(Sql, Args, Context) ->
  201. case q(Sql, Args, Context) of
  202. [Row|_] -> Row;
  203. [] -> undefined
  204. end.
  205. equery(Sql, Context) ->
  206. equery(Sql, [], Context).
  207. equery(Sql, Parameters, Context) ->
  208. case get_connection(Context) of
  209. none ->
  210. {error, noresult};
  211. C ->
  212. try
  213. pgsql:equery(C, Sql, Parameters)
  214. after
  215. return_connection(C, Context)
  216. end
  217. end.
  218. %% @doc Insert a new row in a table, use only default values.
  219. %% @spec insert(Table, Context) -> {ok, Id}
  220. insert(Table, Context) when is_atom(Table) ->
  221. insert(atom_to_list(Table), Context);
  222. insert(Table, Context) ->
  223. assert_table_name(Table),
  224. C = get_connection(Context),
  225. try
  226. pgsql:equery1(C, "insert into \""++Table++"\" default values returning id")
  227. after
  228. return_connection(C, Context)
  229. end.
  230. %% @doc Insert a row, setting the fields to the props. Unknown columns are serialized in the props column.
  231. %% When the table has an 'id' column then the new id is returned.
  232. %% @spec insert(Table::atom(), Props::proplist(), Context) -> {ok, Id} | Error
  233. insert(Table, [], Context) ->
  234. insert(Table, Context);
  235. insert(Table, Props, Context) when is_atom(Table) ->
  236. insert(atom_to_list(Table), Props, Context);
  237. insert(Table, Props, Context) ->
  238. assert_table_name(Table),
  239. Cols = column_names(Table, Context),
  240. InsertProps = prepare_cols(Cols, Props),
  241. InsertProps1 = case proplists:get_value(props, InsertProps) of
  242. undefined ->
  243. InsertProps;
  244. PropsCol ->
  245. lists:keystore(props, 1, InsertProps, {props, cleanup_props(PropsCol)})
  246. end,
  247. %% Build the SQL insert statement
  248. {ColNames,Parameters} = lists:unzip(InsertProps1),
  249. Sql = "insert into \""++Table++"\" (\""
  250. ++ string:join([ atom_to_list(ColName) || ColName <- ColNames ], "\", \"")
  251. ++ "\") values ("
  252. ++ string:join([ [$$ | integer_to_list(N)] || N <- lists:seq(1, length(Parameters)) ], ", ")
  253. ++ ")",
  254. FinalSql = case lists:member(id, Cols) of
  255. true -> Sql ++ " returning id";
  256. false -> Sql
  257. end,
  258. C = get_connection(Context),
  259. try
  260. Id = case pgsql:equery1(C, FinalSql, Parameters) of
  261. {ok, IdVal} -> IdVal;
  262. {error, noresult} -> undefined
  263. end,
  264. {ok, Id}
  265. after
  266. return_connection(C, Context)
  267. end.
  268. %% @doc Update a row in a table, merging the props list with any new props values
  269. %% @spec update(Table, Id, Parameters, Context) -> {ok, RowsUpdated}
  270. update(Table, Id, Parameters, Context) when is_atom(Table) ->
  271. update(atom_to_list(Table), Id, Parameters, Context);
  272. update(Table, Id, Parameters, Context) ->
  273. assert_table_name(Table),
  274. Cols = column_names(Table, Context),
  275. UpdateProps = prepare_cols(Cols, Parameters),
  276. C = get_connection(Context),
  277. try
  278. UpdateProps1 = case proplists:is_defined(props, UpdateProps) of
  279. true ->
  280. % Merge the new props with the props in the database
  281. {ok, OldProps} = pgsql:equery1(C, "select props from \""++Table++"\" where id = $1", [Id]),
  282. case is_list(OldProps) of
  283. true ->
  284. FReplace = fun ({P,_} = T, L) -> lists:keystore(P, 1, L, T) end,
  285. NewProps = lists:foldl(FReplace, OldProps, proplists:get_value(props, UpdateProps)),
  286. lists:keystore(props, 1, UpdateProps, {props, cleanup_props(NewProps)});
  287. false ->
  288. UpdateProps
  289. end;
  290. false ->
  291. UpdateProps
  292. end,
  293. {ColNames,Params} = lists:unzip(UpdateProps1),
  294. ColNamesNr = lists:zip(ColNames, lists:seq(2, length(ColNames)+1)),
  295. Sql = "update \""++Table++"\" set "
  296. ++ string:join([ "\"" ++ atom_to_list(ColName) ++ "\" = $" ++ integer_to_list(Nr) || {ColName, Nr} <- ColNamesNr ], ", ")
  297. ++ " where id = $1",
  298. {ok, RowsUpdated} = pgsql:equery1(C, Sql, [Id | Params]),
  299. {ok, RowsUpdated}
  300. after
  301. return_connection(C, Context)
  302. end.
  303. %% @doc Delete a row from a table, the row must have a column with the name 'id'
  304. %% @spec delete(Table, Id, Context) -> {ok, RowsDeleted}
  305. delete(Table, Id, Context) when is_atom(Table) ->
  306. delete(atom_to_list(Table), Id, Context);
  307. delete(Table, Id, Context) ->
  308. assert_table_name(Table),
  309. C = get_connection(Context),
  310. try
  311. Sql = "delete from \""++Table++"\" where id = $1",
  312. {ok, RowsDeleted} = pgsql:equery1(C, Sql, [Id]),
  313. {ok, RowsDeleted}
  314. after
  315. return_connection(C, Context)
  316. end.
  317. %% @doc Read a row from a table, the row must have a column with the name 'id'.
  318. %% The props column contents is merged with the other properties returned.
  319. %% @spec select(Table, Id, Context) -> {ok, Row}
  320. select(Table, Id, Context) when is_atom(Table) ->
  321. select(atom_to_list(Table), Id, Context);
  322. select(Table, Id, Context) ->
  323. assert_table_name(Table),
  324. C = get_connection(Context),
  325. {ok, Row} = try
  326. Sql = "select * from \""++Table++"\" where id = $1 limit 1",
  327. pgsql:assoc(C, Sql, [Id])
  328. after
  329. return_connection(C, Context)
  330. end,
  331. Props = case Row of
  332. [R] ->
  333. case proplists:get_value(props, R) of
  334. PropsCol when is_list(PropsCol) ->
  335. lists:keydelete(props, 1, R) ++ PropsCol;
  336. _ ->
  337. R
  338. end;
  339. [] ->
  340. []
  341. end,
  342. {ok, Props}.
  343. %% @doc Remove all undefined props, translate texts to binaries.
  344. cleanup_props(Ps) when is_list(Ps) ->
  345. [ {K,to_binary_string(V)} || {K,V} <- Ps, V /= undefined ];
  346. cleanup_props(P) ->
  347. P.
  348. to_binary_string([]) -> [];
  349. to_binary_string(L) when is_list(L) ->
  350. case z_string:is_string(L) of
  351. true -> list_to_binary(L);
  352. false -> L
  353. end;
  354. to_binary_string({trans, Tr}) ->
  355. {trans, [ {Lang,to_binary(V)} || {Lang,V} <- Tr ]};
  356. to_binary_string(V) ->
  357. V.
  358. to_binary(L) when is_list(L) -> list_to_binary(L);
  359. to_binary(V) -> V.
  360. %% @doc Check if all cols are valid columns in the target table, move unknown properties to the props column (if exists)
  361. prepare_cols(Cols, Props) ->
  362. {CProps, PProps} = split_props(Props, Cols),
  363. case PProps of
  364. [] ->
  365. CProps;
  366. _ ->
  367. PPropsMerged = case proplists:is_defined(props, CProps) of
  368. true ->
  369. FReplace = fun ({P,_} = T, L) -> lists:keystore(P, 1, L, T) end,
  370. lists:foldl(FReplace, proplists:get_value(props, CProps), PProps);
  371. false ->
  372. PProps
  373. end,
  374. [{props, PPropsMerged} | proplists:delete(props, CProps)]
  375. end.
  376. split_props(Props, Cols) ->
  377. {CProps, PProps} = lists:partition(fun ({P,_V}) -> lists:member(P, Cols) end, Props),
  378. case PProps of
  379. [] -> ok;
  380. _ -> z_utils:assert(lists:member(props, Cols), {unknown_column, PProps})
  381. end,
  382. {CProps, PProps}.
  383. %% @doc Return a property list with all columns of the table. (example: [{id,int4,modifier},...])
  384. %% @spec columns(Table, Context) -> [ #column_def{} ]
  385. columns(Table, Context) when is_atom(Table) ->
  386. columns(atom_to_list(Table), Context);
  387. columns(Table, Context) ->
  388. {ok, Db} = pgsql_pool:get_database(?HOST(Context)),
  389. {ok, Schema} = pgsql_pool:get_database_opt(schema, ?HOST(Context)),
  390. case z_depcache:get({columns, Db, Schema, Table}, Context) of
  391. {ok, Cols} ->
  392. Cols;
  393. _ ->
  394. Cols = q(" select column_name, data_type, character_maximum_length, is_nullable, column_default
  395. from information_schema.columns
  396. where table_catalog = $1
  397. and table_schema = $2
  398. and table_name = $3
  399. order by ordinal_position", [Db, Schema, Table], Context),
  400. Cols1 = [ columns1(Col) || Col <- Cols ],
  401. z_depcache:set({columns, Db, Schema, Table}, Cols1, ?YEAR, [{database, Db}], Context),
  402. Cols1
  403. end.
  404. columns1({<<"id">>, <<"integer">>, undefined, Nullable, <<"nextval(", _/binary>>}) ->
  405. #column_def{
  406. name = id,
  407. type = "serial",
  408. length = undefined,
  409. is_nullable = z_convert:to_bool(Nullable),
  410. default = undefined
  411. };
  412. columns1({Name,Type,MaxLength,Nullable,Default}) ->
  413. #column_def{
  414. name = z_convert:to_atom(Name),
  415. type = z_convert:to_list(Type),
  416. length = MaxLength,
  417. is_nullable = z_convert:to_bool(Nullable),
  418. default = column_default(Default)
  419. }.
  420. column_default(undefined) -> undefined;
  421. column_default(<<"nextval(", _/binary>>) -> undefined;
  422. column_default(Default) -> binary_to_list(Default).
  423. %% @doc Return a list with the column names of a table. The names are sorted.
  424. %% @spec column_names(Table, Context) -> [ atom() ]
  425. column_names(Table, Context) ->
  426. Names = [ C#column_def.name || C <- columns(Table, Context)],
  427. lists:sort(Names).
  428. %% @doc Flush all cached information about the database.
  429. flush(Context) ->
  430. {ok, Db} = pgsql_pool:get_database(?HOST(Context)),
  431. z_depcache:flush({database, Db}, Context).
  432. %% @doc Update the sequence of the ids in the table. They will be renumbered according to their position in the id list.
  433. %% @spec update_sequence(Table, IdList, Context) -> void()
  434. update_sequence(Table, Ids, Context) when is_atom(Table) ->
  435. update_sequence(atom_to_list(Table), Ids, Context);
  436. update_sequence(Table, Ids, Context) ->
  437. assert_table_name(Table),
  438. Args = lists:zip(Ids, lists:seq(1, length(Ids))),
  439. case get_connection(Context) of
  440. none -> [];
  441. C ->
  442. try
  443. [ {ok, _} = pgsql:equery1(C, "update \""++Table++"\" set seq = $2 where id = $1", Arg) || Arg <- Args ]
  444. after
  445. return_connection(C, Context)
  446. end
  447. end.
  448. %% @doc Check the information schema if a certain table exists in the context database.
  449. %% @spec table_exists(TableName, Context) -> bool()
  450. table_exists(Table, Context) ->
  451. {ok, Db} = pgsql_pool:get_database(?HOST(Context)),
  452. {ok, Schema} = pgsql_pool:get_database_opt(schema, ?HOST(Context)),
  453. case q1(" select count(*)
  454. from information_schema.tables
  455. where table_catalog = $1
  456. and table_name = $2
  457. and table_schema = $3
  458. and table_type = 'BASE TABLE'", [Db, Table, Schema], Context) of
  459. 1 -> true;
  460. 0 -> false
  461. end.
  462. %% @doc Make sure that a table is dropped, only when the table exists
  463. drop_table(Name, Context) when is_atom(Name) ->
  464. drop_table(atom_to_list(Name), Context);
  465. drop_table(Name, Context) ->
  466. case table_exists(Name, Context) of
  467. true -> q("drop table \""++Name++"\"", Context);
  468. false -> ok
  469. end.
  470. %% @doc Ensure that a table with the given columns exists, alter any existing table
  471. %% to add, modify or drop columns. The 'id' (with type serial) column _must_ be defined
  472. %% when creating the table.
  473. ensure_table(Table, Cols, Context) when is_atom(Table) ->
  474. ensure_table(atom_to_list(Table), Cols, Context);
  475. ensure_table(Table, Cols, Context) ->
  476. case table_exists(Table, Context) of
  477. false ->
  478. ensure_table_create(Table, Cols, Context);
  479. true ->
  480. ExistingCols = lists:sort(columns(Table, Context)),
  481. WantedCols = lists:sort(Cols),
  482. case ensure_table_alter_cols(WantedCols, ExistingCols, []) of
  483. [] -> ok;
  484. Diff ->
  485. {ok, Db} = pgsql_pool:get_database(?HOST(Context)),
  486. {ok, Schema} = pgsql_pool:get_database_opt(schema, ?HOST(Context)),
  487. z_db:q("ALTER TABLE \""++Table++"\" " ++ string:join(Diff, ","), Context),
  488. z_depcache:flush({columns, Db, Schema, Table}, Context),
  489. ok
  490. end
  491. end.
  492. ensure_table_create(Name, Cols, Context) ->
  493. ColsSQL = ensure_table_create_cols(Cols, []),
  494. z_db:q("CREATE TABLE \""++Name++"\" ("++string:join(ColsSQL, ",") ++ table_create_primary_key(Cols) ++ ")", Context),
  495. ok.
  496. table_create_primary_key([]) -> [];
  497. table_create_primary_key([#column_def{name=id, type="serial"}|_]) -> ", primary key(id)";
  498. table_create_primary_key([_|Cols]) -> table_create_primary_key(Cols).
  499. ensure_table_create_cols([], Acc) ->
  500. lists:reverse(Acc);
  501. ensure_table_create_cols([C|Cols], Acc) ->
  502. M = lists:flatten([$", atom_to_list(C#column_def.name), $", 32, column_spec(C)]),
  503. ensure_table_create_cols(Cols, [M|Acc]).
  504. ensure_table_alter_cols([], [], Acc) ->
  505. lists:reverse(Acc);
  506. ensure_table_alter_cols([N|Ns], [N|Es], Acc) ->
  507. ensure_table_alter_cols(Ns, Es, Acc);
  508. ensure_table_alter_cols([N|Ns], [E|Es], Acc) when N#column_def.name == E#column_def.name ->
  509. M = lists:flatten(["ALTER COLUMN \"", atom_to_list(N#column_def.name), "\" TYPE ", column_spec(N)]),
  510. M1 = case N#column_def.is_nullable of
  511. true -> M ++ lists:flatten([", ALTER COLUMN \"", atom_to_list(N#column_def.name), "\" DROP NOT NULL"]);
  512. false -> M ++ lists:flatten([", ALTER COLUMN \"", atom_to_list(N#column_def.name), "\" SET NOT NULL"])
  513. end,
  514. M2 = case N#column_def.default of
  515. undefined -> M1 ++ lists:flatten([", ALTER COLUMN \"", atom_to_list(N#column_def.name), "\" DROP DEFAULT"]);
  516. Default -> M1 ++ lists:flatten([", ALTER COLUMN \"", atom_to_list(N#column_def.name), "\" SET DEFAULT ", Default])
  517. end,
  518. ensure_table_alter_cols(Ns, Es, [M2|Acc]);
  519. ensure_table_alter_cols([N|Ns], Es, Acc) when Es == [] orelse N < hd(Es) ->
  520. M = lists:flatten(["ADD COLUMN \"", atom_to_list(N#column_def.name), "\" ",
  521. column_spec(N),
  522. column_spec_nullable(N#column_def.is_nullable),
  523. column_spec_default(N#column_def.default)]),
  524. ensure_table_alter_cols(Ns, Es, [M|Acc]);
  525. ensure_table_alter_cols(Ns, [E|Es], Acc) when Ns == [] orelse E < hd(Ns) ->
  526. M = lists:flatten(["DROP COLUMN \"", atom_to_list(E#column_def.name), "\""]),
  527. ensure_table_alter_cols(Ns, Es, [M|Acc]).
  528. column_spec(#column_def{type=Type, length=undefined}) ->
  529. Type;
  530. column_spec(#column_def{type=Type, length=Length}) ->
  531. lists:flatten([Type, $(, integer_to_list(Length), $)]).
  532. column_spec_nullable(true) -> "";
  533. column_spec_nullable(false) -> " not null".
  534. column_spec_default(undefined) -> "";
  535. column_spec_default(Default) -> [32, Default].
  536. %% @doc Check if a name is a valid SQL table name. Crashes when invalid
  537. %% @spec assert_table_name(String) -> true
  538. assert_table_name([H|T]) when (H >= $a andalso H =< $z) orelse H == $_ ->
  539. assert_table_name1(T).
  540. assert_table_name1([]) ->
  541. true;
  542. assert_table_name1([H|T]) when (H >= $a andalso H =< $z) orelse (H >= $0 andalso H =< $9) orelse H == $_ ->
  543. assert_table_name1(T).
  544. %% @doc Merge the contents of the props column into the result rows
  545. %% @spec merge_props(list()) -> list()
  546. merge_props(undefined) ->
  547. undefined;
  548. merge_props(List) ->
  549. merge_props(List, []).
  550. merge_props([], Acc) ->
  551. lists:reverse(Acc);
  552. merge_props([R|Rest], Acc) ->
  553. case proplists:get_value(props, R) of
  554. undefined ->
  555. merge_props(Rest, [R|Acc]);
  556. <<>> ->
  557. merge_props(Rest, [R|Acc]);
  558. List ->
  559. merge_props(Rest, [lists:keydelete(props, 1, R)++List|Acc])
  560. end.