Skip to content

Commit 94dae34

Browse files
committed
Add internal json/jsonb support to flex output
Table columns with type 'json' or 'jsonb' are now explicitly supported. Lua booleans, strings, numbers, and tables can be written to those columns and are automatically encoded as JSON on the fly. This is a breaking change to the behaviour we had before where you had to encode the JSON yourself in Lua before giving it to osm2pgsql. We can still do this change as the flex backend is still marked experimental and the behaviour is much cleaner this way then if we keep backwards compatibility.
1 parent 6fc4747 commit 94dae34

File tree

6 files changed

+233
-44
lines changed

6 files changed

+233
-44
lines changed

flex-config/places.lua

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,7 @@
22

33
-- This example shows how you can use JSON and JSONB columns in your database.
44

5-
-- You need the 'dkjson' JSON library for Lua. Install 'dkjson' with LuaRocks
6-
-- or install from http://dkolf.de/src/dkjson-lua.fsl/home . Debian/Ubuntu
7-
-- users can install the 'lua-dkjson' package.
8-
9-
-- Use JSON encoder
10-
local json = require('dkjson')
11-
125
local places = osm2pgsql.define_node_table('places', {
13-
-- The jsonb column is handled by osm2pgsql just like a normal text
14-
-- column. It is the job of the Lua script to put correct json there.
156
{ column = 'tags', type = 'jsonb' },
167
{ column = 'geom', type = 'point' },
178
})
@@ -46,8 +37,13 @@ function osm2pgsql.process_node(object)
4637
object.tags.names = names
4738
end
4839

40+
-- The population should be stored as number, not as a string
41+
if object.tags.population then
42+
object.tags.population = tonumber(object.tags.population)
43+
end
44+
4945
places:add_row({
50-
tags = json.encode(object.tags)
46+
tags = object.tags
5147
})
5248
end
5349

src/flex-table-column.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct column_type_lookup
2222
table_column_type type;
2323
};
2424

25-
static std::array<column_type_lookup, 24> const column_types = {
25+
static std::array<column_type_lookup, 26> const column_types = {
2626
{{"sql", table_column_type::sql},
2727
{"text", table_column_type::text},
2828
{"boolean", table_column_type::boolean},
@@ -36,6 +36,8 @@ static std::array<column_type_lookup, 24> const column_types = {
3636
{"bigint", table_column_type::int8},
3737
{"real", table_column_type::real},
3838
{"hstore", table_column_type::hstore},
39+
{"json", table_column_type::json},
40+
{"jsonb", table_column_type::jsonb},
3941
{"direction", table_column_type::direction},
4042
{"geometry", table_column_type::geometry},
4143
{"point", table_column_type::point},
@@ -130,6 +132,10 @@ std::string flex_table_column_t::sql_type_name() const
130132
return "real";
131133
case table_column_type::hstore:
132134
return "hstore";
135+
case table_column_type::json:
136+
return "json";
137+
case table_column_type::jsonb:
138+
return "jsonb";
133139
case table_column_type::direction:
134140
return "int2";
135141
case table_column_type::geometry:

src/flex-table-column.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ enum class table_column_type : uint8_t
3131
real,
3232

3333
hstore,
34+
json,
35+
jsonb,
3436

3537
direction,
3638

src/output-flex.cpp

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ extern "C"
3030
#include <lualib.h>
3131
}
3232

33+
#include <rapidjson/stringbuffer.h>
34+
#include <rapidjson/writer.h>
35+
3336
#include <cassert>
3437
#include <cstdio>
3538
#include <cstdlib>
@@ -318,6 +321,122 @@ static void write_double(db_copy_mgr_t<db_deleter_by_type_and_id_t> *copy_mgr,
318321
copy_mgr->add_column(value);
319322
}
320323

324+
using json_writer_type = rapidjson::Writer<rapidjson::StringBuffer>;
325+
using table_register_type = std::vector<void const *>;
326+
327+
/**
328+
* Check that the value on the top of the Lua stack is a simple array.
329+
* This means that all keys must be consecutive integers starting from 1.
330+
*/
331+
static bool is_lua_array(lua_State *lua_state)
332+
{
333+
uint32_t n = 1;
334+
lua_pushnil(lua_state);
335+
while (lua_next(lua_state, -2) != 0) {
336+
lua_pop(lua_state, 1); // remove value from stack
337+
if (!lua_isinteger(lua_state, -1)) {
338+
lua_pop(lua_state, 1);
339+
return false;
340+
}
341+
int okay = 0;
342+
auto const num = lua_tointegerx(lua_state, -1, &okay);
343+
if (!okay || num != n++) {
344+
lua_pop(lua_state, 1);
345+
return false;
346+
}
347+
}
348+
349+
// An empty lua table could be both, we decide here that it is not stored
350+
// as a JSON array but as a JSON object.
351+
if (n == 1) {
352+
return false;
353+
}
354+
355+
return true;
356+
}
357+
358+
static void write_json(json_writer_type *writer, lua_State *lua_state,
359+
table_register_type *tables);
360+
361+
static void write_json_table(json_writer_type *writer, lua_State *lua_state,
362+
table_register_type *tables)
363+
{
364+
void const *table_ptr = lua_topointer(lua_state, -1);
365+
assert(table_ptr);
366+
auto const it = std::find(tables->cbegin(), tables->cend(), table_ptr);
367+
if (it != tables->cend()) {
368+
throw std::runtime_error{"Loop detected in table"};
369+
}
370+
tables->push_back(table_ptr);
371+
372+
if (is_lua_array(lua_state)) {
373+
writer->StartArray();
374+
lua_pushnil(lua_state);
375+
while (lua_next(lua_state, -2) != 0) {
376+
write_json(writer, lua_state, tables);
377+
lua_pop(lua_state, 1);
378+
}
379+
writer->EndArray();
380+
} else {
381+
writer->StartObject();
382+
lua_pushnil(lua_state);
383+
while (lua_next(lua_state, -2) != 0) {
384+
int const ltype_key = lua_type(lua_state, -2);
385+
if (ltype_key != LUA_TSTRING) {
386+
throw std::runtime_error{
387+
"Incorrect data type '{}' as key."_format(
388+
lua_typename(lua_state, ltype_key))};
389+
}
390+
char const *const key = lua_tostring(lua_state, -2);
391+
writer->Key(key);
392+
write_json(writer, lua_state, tables);
393+
lua_pop(lua_state, 1);
394+
}
395+
writer->EndObject();
396+
}
397+
}
398+
399+
static void write_json_number(json_writer_type *writer, lua_State *lua_state)
400+
{
401+
int okay = 0;
402+
auto const num = lua_tointegerx(lua_state, -1, &okay);
403+
if (okay) {
404+
writer->Int64(num);
405+
} else {
406+
writer->Double(lua_tonumber(lua_state, -1));
407+
}
408+
}
409+
410+
static void write_json(json_writer_type *writer, lua_State *lua_state,
411+
table_register_type *tables)
412+
{
413+
assert(writer);
414+
assert(lua_state);
415+
416+
int const ltype = lua_type(lua_state, -1);
417+
switch (ltype) {
418+
case LUA_TNIL:
419+
writer->Null();
420+
break;
421+
case LUA_TBOOLEAN:
422+
writer->Bool(lua_toboolean(lua_state, -1) != 0);
423+
break;
424+
case LUA_TNUMBER:
425+
write_json_number(writer, lua_state);
426+
break;
427+
case LUA_TSTRING:
428+
writer->String(lua_tostring(lua_state, -1));
429+
break;
430+
case LUA_TTABLE:
431+
write_json_table(writer, lua_state, tables);
432+
break;
433+
default:
434+
throw std::runtime_error{
435+
"Invalid type '{}' for json/jsonb column."_format(
436+
lua_typename(lua_state, ltype))};
437+
}
438+
}
439+
321440
void output_flex_t::write_column(
322441
db_copy_mgr_t<db_deleter_by_type_and_id_t> *copy_mgr,
323442
flex_table_column_t const &column)
@@ -467,6 +586,13 @@ void output_flex_t::write_column(
467586
"Invalid type '{}' for hstore column."_format(
468587
lua_typename(lua_state(), ltype))};
469588
}
589+
} else if ((column.type() == table_column_type::json) ||
590+
(column.type() == table_column_type::jsonb)) {
591+
rapidjson::StringBuffer stream;
592+
json_writer_type writer{stream};
593+
table_register_type tables;
594+
write_json(&writer, lua_state(), &tables);
595+
copy_mgr->add_column(stream.GetString());
470596
} else if (column.type() == table_column_type::direction) {
471597
switch (ltype) {
472598
case LUA_TBOOLEAN:

tests/data/test_output_flex_types.lua

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ local test_table = osm2pgsql.define_node_table("nodes", {
77
{ column = 'tint8', type = 'int8' },
88
{ column = 'treal', type = 'real' },
99
{ column = 'thstr', type = 'hstore' },
10+
{ column = 'tjson', type = 'jsonb' },
1011
{ column = 'tdirn', type = 'direction' },
1112
{ column = 'tsqlt', type = 'varchar' },
1213
})
@@ -20,11 +21,11 @@ function osm2pgsql.process_node(object)
2021
end
2122
if test_type == 'boolean' then
2223
local row = { tbool = true, tint2 = true, tint4 = true,
23-
tint8 = true, tdirn = true }
24+
tint8 = true, tjson = true, tdirn = true }
2425
test_table:add_row(row)
2526

2627
row = { tbool = false, tint2 = false, tint4 = false,
27-
tint8 = false, tdirn = false }
28+
tint8 = false, tjson = false, tdirn = false }
2829
test_table:add_row(row)
2930
return
3031
end
@@ -46,6 +47,7 @@ function osm2pgsql.process_node(object)
4647
tint4 = n,
4748
tint8 = n,
4849
treal = n,
50+
tjson = n,
4951
tdirn = n,
5052
tsqlt = n,
5153
}
@@ -129,8 +131,9 @@ function osm2pgsql.process_node(object)
129131
return
130132
end
131133
if test_type == 'table' then
132-
test_table:add_row{ thstr = {} }
133-
test_table:add_row{ thstr = { a = 'b', c = 'd' } }
134+
local t = { a = 'b', c = 'd' }
135+
test_table:add_row{ thstr = {}, tjson = {} }
136+
test_table:add_row{ thstr = t, tjson = t }
134137
return
135138
end
136139
if test_type == 'table-hstore-fail' then
@@ -141,5 +144,24 @@ function osm2pgsql.process_node(object)
141144
test_table:add_row{ [object.tags.column] = {} }
142145
return
143146
end
147+
if test_type == 'json' then
148+
test_table:add_row{ tjson = {
149+
astring = '123',
150+
aninteger = 124,
151+
anumber = 12.5,
152+
atrue = true,
153+
afalse = false,
154+
anull = nil,
155+
atable = { a = 'nested', tab = 'le' },
156+
anarray = { 4, 3, 7 }
157+
}}
158+
return
159+
end
160+
if test_type == 'json-loop' then
161+
local atable = { a = 'b' }
162+
atable.c = atable
163+
test_table:add_row{ tjson = atable }
164+
return
165+
end
144166
end
145167

0 commit comments

Comments
 (0)