Skip to content

Commit 4c55e6e

Browse files
authored
Move INSERT query of repack.repack_trigger to C (#368)
Move INSERT query of repack.repack_trigger to C Making INSERT query in C prevents SQL injections, which are possible if an INSERT query is passed as an argument. Additionally this commit adds funtions: - repack.create_index_type - repack.create_log_table - repack.create_table Another change is that tablespace_orig considers default tablespace of a database.
1 parent 6263dd5 commit 4c55e6e

13 files changed

+201
-40
lines changed

META.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "pg_repack",
33
"abstract": "PostgreSQL module for data reorganization",
44
"description": "Reorganize tables in PostgreSQL databases with minimal locks",
5-
"version": "1.4.8",
5+
"version": "1.4.9",
66
"maintainer": [
77
"Beena Emerson <[email protected]>",
88
"Josh Kupershmidt <[email protected]>",

bin/pg_repack.c

+8-18
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ typedef struct repack_table
194194
const char *create_trigger; /* CREATE TRIGGER repack_trigger */
195195
const char *enable_trigger; /* ALTER TABLE ENABLE ALWAYS TRIGGER repack_trigger */
196196
const char *create_table; /* CREATE TABLE table AS SELECT WITH NO DATA*/
197+
const char *dest_tablespace; /* Destination tablespace */
197198
const char *copy_data; /* INSERT INTO */
198199
const char *alter_col_storage; /* ALTER TABLE ALTER COLUMN SET STORAGE */
199200
const char *drop_columns; /* ALTER TABLE DROP COLUMNs */
@@ -893,9 +894,6 @@ repack_one_database(const char *orderby, char *errbuf, size_t errsize)
893894
{
894895
repack_table table;
895896
StringInfoData copy_sql;
896-
const char *create_table_1;
897-
const char *create_table_2;
898-
const char *tablespace;
899897
const char *ckey;
900898
int c = 0;
901899

@@ -919,9 +917,8 @@ repack_one_database(const char *orderby, char *errbuf, size_t errsize)
919917
table.create_trigger = getstr(res, i, c++);
920918
table.enable_trigger = getstr(res, i, c++);
921919

922-
create_table_1 = getstr(res, i, c++);
923-
tablespace = getstr(res, i, c++); /* to be clobbered */
924-
create_table_2 = getstr(res, i, c++);
920+
table.create_table = getstr(res, i, c++);
921+
getstr(res, i, c++); /* tablespace_orig is clobbered */
925922
table.copy_data = getstr(res, i , c++);
926923
table.alter_col_storage = getstr(res, i, c++);
927924
table.drop_columns = getstr(res, i, c++);
@@ -933,17 +930,7 @@ repack_one_database(const char *orderby, char *errbuf, size_t errsize)
933930
table.sql_delete = getstr(res, i, c++);
934931
table.sql_update = getstr(res, i, c++);
935932
table.sql_pop = getstr(res, i, c++);
936-
tablespace = getstr(res, i, c++);
937-
938-
/* Craft CREATE TABLE SQL */
939-
resetStringInfo(&sql);
940-
appendStringInfoString(&sql, create_table_1);
941-
appendStringInfoString(&sql, tablespace);
942-
appendStringInfoString(&sql, create_table_2);
943-
944-
/* Always append WITH NO DATA to CREATE TABLE SQL*/
945-
appendStringInfoString(&sql, " WITH NO DATA");
946-
table.create_table = sql.data;
933+
table.dest_tablespace = getstr(res, i, c++);
947934

948935
/* Craft Copy SQL */
949936
initStringInfo(&copy_sql);
@@ -1268,6 +1255,7 @@ repack_one_table(repack_table *table, const char *orderby)
12681255
elog(DEBUG2, "create_trigger : %s", table->create_trigger);
12691256
elog(DEBUG2, "enable_trigger : %s", table->enable_trigger);
12701257
elog(DEBUG2, "create_table : %s", table->create_table);
1258+
elog(DEBUG2, "dest_tablespace : %s", table->dest_tablespace);
12711259
elog(DEBUG2, "copy_data : %s", table->copy_data);
12721260
elog(DEBUG2, "alter_col_storage : %s", table->alter_col_storage ?
12731261
table->alter_col_storage : "(skipped)");
@@ -1530,7 +1518,9 @@ repack_one_table(repack_table *table, const char *orderby)
15301518
* Before copying data to the target table, we need to set the column storage
15311519
* type if its storage type has been changed from the type default.
15321520
*/
1533-
command(table->create_table, 0, NULL);
1521+
params[0] = utoa(table->target_oid, buffer);
1522+
params[1] = table->dest_tablespace;
1523+
command(table->create_table, 2, params);
15341524
if (table->alter_col_storage)
15351525
command(table->alter_col_storage, 0, NULL);
15361526
command(table->copy_data, 0, NULL);

lib/pg_repack.sql.in

+49-12
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,14 @@ $$
2424
$$
2525
LANGUAGE sql STABLE STRICT SET search_path to 'pg_catalog';
2626

27-
CREATE FUNCTION repack.get_index_columns(oid, text) RETURNS text AS
27+
-- Get a comma-separated column list of the index.
28+
--
29+
-- Columns are quoted as literals because they are going to be passed to
30+
-- the `repack_trigger` function as text arguments. `repack_trigger` will quote
31+
-- them as identifiers later.
32+
CREATE FUNCTION repack.get_index_columns(oid) RETURNS text AS
2833
$$
29-
SELECT coalesce(string_agg(quote_ident(attname), $2), '')
34+
SELECT coalesce(string_agg(quote_literal(attname), ', '), '')
3035
FROM pg_attribute,
3136
(SELECT indrelid,
3237
indkey,
@@ -43,6 +48,37 @@ CREATE FUNCTION repack.get_order_by(oid, oid) RETURNS text AS
4348
'MODULE_PATHNAME', 'repack_get_order_by'
4449
LANGUAGE C STABLE STRICT;
4550

51+
CREATE FUNCTION repack.create_log_table(oid) RETURNS void AS
52+
$$
53+
BEGIN
54+
EXECUTE 'CREATE TABLE repack.log_' || $1 ||
55+
' (id bigserial PRIMARY KEY,' ||
56+
' pk repack.pk_' || $1 || ',' ||
57+
' row ' || repack.oid2text($1) || ')';
58+
END
59+
$$
60+
LANGUAGE plpgsql;
61+
62+
CREATE FUNCTION repack.create_table(oid, name) RETURNS void AS
63+
$$
64+
BEGIN
65+
EXECUTE 'CREATE TABLE repack.table_' || $1 ||
66+
' WITH (' || repack.get_storage_param($1) || ') ' ||
67+
' TABLESPACE ' || quote_ident($2) ||
68+
' AS SELECT ' || repack.get_columns_for_create_as($1) ||
69+
' FROM ONLY ' || repack.oid2text($1) || ' WITH NO DATA';
70+
END
71+
$$
72+
LANGUAGE plpgsql;
73+
74+
CREATE FUNCTION repack.create_index_type(oid, oid) RETURNS void AS
75+
$$
76+
BEGIN
77+
EXECUTE repack.get_create_index_type($1, 'repack.pk_' || $2);
78+
END
79+
$$
80+
LANGUAGE plpgsql;
81+
4682
CREATE FUNCTION repack.get_create_index_type(oid, name) RETURNS text AS
4783
$$
4884
SELECT 'CREATE TYPE ' || $2 || ' AS (' ||
@@ -66,10 +102,7 @@ $$
66102
SELECT 'CREATE TRIGGER repack_trigger' ||
67103
' AFTER INSERT OR DELETE OR UPDATE ON ' || repack.oid2text($1) ||
68104
' FOR EACH ROW EXECUTE PROCEDURE repack.repack_trigger(' ||
69-
'''INSERT INTO repack.log_' || $1 || '(pk, row) VALUES(' ||
70-
' CASE WHEN $1 IS NULL THEN NULL ELSE (ROW($1.' ||
71-
repack.get_index_columns($2, ', $1.') || ')::repack.pk_' ||
72-
$1 || ') END, $2)'')';
105+
repack.get_index_columns($2) || ')';
73106
$$
74107
LANGUAGE sql STABLE STRICT;
75108

@@ -240,13 +273,12 @@ CREATE VIEW repack.tables AS
240273
N.nspname AS schemaname,
241274
PK.indexrelid AS pkid,
242275
CK.indexrelid AS ckid,
243-
repack.get_create_index_type(PK.indexrelid, 'repack.pk_' || R.oid) AS create_pktype,
244-
'CREATE TABLE repack.log_' || R.oid || ' (id bigserial PRIMARY KEY, pk repack.pk_' || R.oid || ', row ' || repack.oid2text(R.oid) || ')' AS create_log,
276+
'SELECT repack.create_index_type(' || PK.indexrelid || ',' || R.oid || ')' AS create_pktype,
277+
'SELECT repack.create_log_table(' || R.oid || ')' AS create_log,
245278
repack.get_create_trigger(R.oid, PK.indexrelid) AS create_trigger,
246279
repack.get_enable_trigger(R.oid) as enable_trigger,
247-
'CREATE TABLE repack.table_' || R.oid || ' WITH (' || repack.get_storage_param(R.oid) || ') TABLESPACE ' AS create_table_1,
248-
coalesce(quote_ident(S.spcname), 'pg_default') as tablespace_orig,
249-
' AS SELECT ' || repack.get_columns_for_create_as(R.oid) || ' FROM ONLY ' || repack.oid2text(R.oid) AS create_table_2,
280+
'SELECT repack.create_table($1, $2)' AS create_table,
281+
coalesce(S.spcname, S2.spcname) AS tablespace_orig,
250282
'INSERT INTO repack.table_' || R.oid || ' SELECT ' || repack.get_columns_for_create_as(R.oid) || ' FROM ONLY ' || repack.oid2text(R.oid) AS copy_data,
251283
repack.get_alter_col_storage(R.oid) AS alter_col_storage,
252284
repack.get_drop_columns(R.oid, 'repack.table_' || R.oid) AS drop_columns,
@@ -270,6 +302,10 @@ CREATE VIEW repack.tables AS
270302
ON R.oid = CK.indrelid
271303
LEFT JOIN pg_namespace N ON N.oid = R.relnamespace
272304
LEFT JOIN pg_tablespace S ON S.oid = R.reltablespace
305+
CROSS JOIN (SELECT S2.spcname
306+
FROM pg_catalog.pg_database D
307+
JOIN pg_catalog.pg_tablespace S2 ON S2.oid = D.dattablespace
308+
WHERE D.datname = current_database()) S2
273309
WHERE R.relkind = 'r'
274310
AND R.relpersistence = 'p'
275311
AND N.nspname NOT IN ('pg_catalog', 'information_schema')
@@ -281,7 +317,8 @@ LANGUAGE C STABLE;
281317

282318
CREATE FUNCTION repack.repack_trigger() RETURNS trigger AS
283319
'MODULE_PATHNAME', 'repack_trigger'
284-
LANGUAGE C VOLATILE STRICT SECURITY DEFINER;
320+
LANGUAGE C VOLATILE STRICT SECURITY DEFINER
321+
SET search_path = pg_catalog, pg_temp;
285322

286323
CREATE FUNCTION repack.conflicted_triggers(oid) RETURNS SETOF name AS
287324
$$

lib/repack.c

+21-8
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,11 @@ repack_version(PG_FUNCTION_ARGS)
145145
* @fn Datum repack_trigger(PG_FUNCTION_ARGS)
146146
* @brief Insert a operation log into log-table.
147147
*
148-
* repack_trigger(sql)
148+
* repack_trigger(column1, ..., columnN)
149149
*
150-
* @param sql SQL to insert a operation log into log-table.
150+
* @param column1 A column of the table in primary key/unique index.
151+
* ...
152+
* @param columnN A column of the table in primary key/unique index.
151153
*/
152154
Datum
153155
repack_trigger(PG_FUNCTION_ARGS)
@@ -158,7 +160,8 @@ repack_trigger(PG_FUNCTION_ARGS)
158160
Datum values[2];
159161
bool nulls[2] = { 0, 0 };
160162
Oid argtypes[2];
161-
const char *sql;
163+
Oid relid;
164+
StringInfo sql;
162165

163166
/* authority check */
164167
must_be_superuser("repack_trigger");
@@ -167,11 +170,12 @@ repack_trigger(PG_FUNCTION_ARGS)
167170
if (!CALLED_AS_TRIGGER(fcinfo) ||
168171
!TRIGGER_FIRED_AFTER(trigdata->tg_event) ||
169172
!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event) ||
170-
trigdata->tg_trigger->tgnargs != 1)
173+
trigdata->tg_trigger->tgnargs < 1)
171174
elog(ERROR, "repack_trigger: invalid trigger call");
172175

176+
relid = RelationGetRelid(trigdata->tg_relation);
177+
173178
/* retrieve parameters */
174-
sql = trigdata->tg_trigger->tgargs[0];
175179
desc = RelationGetDescr(trigdata->tg_relation);
176180
argtypes[0] = argtypes[1] = trigdata->tg_relation->rd_rel->reltype;
177181

@@ -200,8 +204,17 @@ repack_trigger(PG_FUNCTION_ARGS)
200204
values[1] = copy_tuple(tuple, desc);
201205
}
202206

203-
/* INSERT INTO repack.log VALUES ($1, $2) */
204-
execute_with_args(SPI_OK_INSERT, sql, 2, argtypes, values, nulls);
207+
/* prepare INSERT query */
208+
sql = makeStringInfo();
209+
appendStringInfo(sql, "INSERT INTO repack.log_%d(pk, row) "
210+
"VALUES(CASE WHEN $1 IS NULL THEN NULL ELSE (ROW(", relid);
211+
appendStringInfo(sql, "$1.%s", quote_identifier(trigdata->tg_trigger->tgargs[0]));
212+
for (int i = 1; i < trigdata->tg_trigger->tgnargs; ++i)
213+
appendStringInfo(sql, ", $1.%s", quote_identifier(trigdata->tg_trigger->tgargs[i]));
214+
appendStringInfo(sql, ")::repack.pk_%d) END, $2)", relid);
215+
216+
/* execute the INSERT query */
217+
execute_with_args(SPI_OK_INSERT, sql->data, 2, argtypes, values, nulls);
205218

206219
SPI_finish();
207220

@@ -794,7 +807,7 @@ repack_indexdef(PG_FUNCTION_ARGS)
794807
/* specify the new tablespace or the original one if any */
795808
if (tablespace || stmt.tablespace)
796809
appendStringInfo(&str, " TABLESPACE %s",
797-
(tablespace ? NameStr(*tablespace) : stmt.tablespace));
810+
(tablespace ? quote_identifier(NameStr(*tablespace)) : stmt.tablespace));
798811

799812
if (stmt.where)
800813
appendStringInfo(&str, " WHERE %s", stmt.where);

regress/Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ INTVERSION := $(shell echo $$(($$(echo $(VERSION).0 | sed 's/\([[:digit:]]\{1,\}
1717
# Test suite
1818
#
1919

20-
REGRESS := init-extension repack-setup repack-run error-on-invalid-idx after-schema repack-check nosuper tablespace get_order_by
20+
REGRESS := init-extension repack-setup repack-run error-on-invalid-idx after-schema repack-check nosuper tablespace get_order_by trigger
2121

2222
USE_PGXS = 1 # use pgxs if not in contrib directory
2323
PGXS := $(shell $(PG_CONFIG) --pgxs)

regress/expected/tablespace.out

+13
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ WHERE indrelid = 'testts1'::regclass ORDER BY relname;
6666
CREATE INDEX CONCURRENTLY index_OID ON testts1 USING btree (id) WITH (fillfactor='80') TABLESPACE foo
6767
(3 rows)
6868

69+
-- Test that a tablespace is quoted as an identifier
70+
SELECT regexp_replace(
71+
repack.repack_indexdef(indexrelid, 'testts1'::regclass, 'foo bar', false),
72+
'_[0-9]+', '_OID', 'g')
73+
FROM pg_index i join pg_class c ON c.oid = indexrelid
74+
WHERE indrelid = 'testts1'::regclass ORDER BY relname;
75+
regexp_replace
76+
---------------------------------------------------------------------------------------------------------
77+
CREATE INDEX index_OID ON repack.table_OID USING btree (id) TABLESPACE "foo bar" WHERE (id > 0)
78+
CREATE UNIQUE INDEX index_OID ON repack.table_OID USING btree (id) TABLESPACE "foo bar"
79+
CREATE INDEX index_OID ON repack.table_OID USING btree (id) WITH (fillfactor='80') TABLESPACE "foo bar"
80+
(3 rows)
81+
6982
-- can move the tablespace from default
7083
\! pg_repack --dbname=contrib_regression --no-order --table=testts1 --tablespace testts
7184
INFO: repacking table "public.testts1"

regress/expected/tablespace_1.out

+13
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ WHERE indrelid = 'testts1'::regclass ORDER BY relname;
6666
CREATE INDEX CONCURRENTLY index_OID ON testts1 USING btree (id) WITH (fillfactor='80') TABLESPACE foo
6767
(3 rows)
6868

69+
-- Test that a tablespace is quoted as an identifier
70+
SELECT regexp_replace(
71+
repack.repack_indexdef(indexrelid, 'testts1'::regclass, 'foo bar', false),
72+
'_[0-9]+', '_OID', 'g')
73+
FROM pg_index i join pg_class c ON c.oid = indexrelid
74+
WHERE indrelid = 'testts1'::regclass ORDER BY relname;
75+
regexp_replace
76+
---------------------------------------------------------------------------------------------------------
77+
CREATE INDEX index_OID ON repack.table_OID USING btree (id) TABLESPACE "foo bar" WHERE (id > 0)
78+
CREATE UNIQUE INDEX index_OID ON repack.table_OID USING btree (id) TABLESPACE "foo bar"
79+
CREATE INDEX index_OID ON repack.table_OID USING btree (id) WITH (fillfactor='80') TABLESPACE "foo bar"
80+
(3 rows)
81+
6982
-- can move the tablespace from default
7083
\! pg_repack --dbname=contrib_regression --no-order --table=testts1 --tablespace testts
7184
INFO: repacking table "public.testts1"

regress/expected/tablespace_2.out

+13
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ WHERE indrelid = 'testts1'::regclass ORDER BY relname;
6666
CREATE INDEX CONCURRENTLY index_OID ON public.testts1 USING btree (id) WITH (fillfactor='80') TABLESPACE foo
6767
(3 rows)
6868

69+
-- Test that a tablespace is quoted as an identifier
70+
SELECT regexp_replace(
71+
repack.repack_indexdef(indexrelid, 'testts1'::regclass, 'foo bar', false),
72+
'_[0-9]+', '_OID', 'g')
73+
FROM pg_index i join pg_class c ON c.oid = indexrelid
74+
WHERE indrelid = 'testts1'::regclass ORDER BY relname;
75+
regexp_replace
76+
---------------------------------------------------------------------------------------------------------
77+
CREATE INDEX index_OID ON repack.table_OID USING btree (id) TABLESPACE "foo bar" WHERE (id > 0)
78+
CREATE UNIQUE INDEX index_OID ON repack.table_OID USING btree (id) TABLESPACE "foo bar"
79+
CREATE INDEX index_OID ON repack.table_OID USING btree (id) WITH (fillfactor='80') TABLESPACE "foo bar"
80+
(3 rows)
81+
6982
-- can move the tablespace from default
7083
\! pg_repack --dbname=contrib_regression --no-order --table=testts1 --tablespace testts
7184
INFO: repacking table "public.testts1"

regress/expected/tablespace_3.out

+13
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ WHERE indrelid = 'testts1'::regclass ORDER BY relname;
6666
CREATE INDEX CONCURRENTLY index_OID ON testts1 USING btree (id) WITH (fillfactor=80) TABLESPACE foo
6767
(3 rows)
6868

69+
-- Test that a tablespace is quoted as an identifier
70+
SELECT regexp_replace(
71+
repack.repack_indexdef(indexrelid, 'testts1'::regclass, 'foo bar', false),
72+
'_[0-9]+', '_OID', 'g')
73+
FROM pg_index i join pg_class c ON c.oid = indexrelid
74+
WHERE indrelid = 'testts1'::regclass ORDER BY relname;
75+
regexp_replace
76+
---------------------------------------------------------------------------------------------------------
77+
CREATE INDEX index_OID ON repack.table_OID USING btree (id) TABLESPACE "foo bar" WHERE (id > 0)
78+
CREATE UNIQUE INDEX index_OID ON repack.table_OID USING btree (id) TABLESPACE "foo bar"
79+
CREATE INDEX index_OID ON repack.table_OID USING btree (id) WITH (fillfactor='80') TABLESPACE "foo bar"
80+
(3 rows)
81+
6982
-- can move the tablespace from default
7083
\! pg_repack --dbname=contrib_regression --no-order --table=testts1 --tablespace testts
7184
INFO: repacking table "public.testts1"

regress/expected/tablespace_4.out

+13
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ WHERE indrelid = 'testts1'::regclass ORDER BY relname;
6666
CREATE INDEX CONCURRENTLY index_OID ON public.testts1 USING btree (id) WITH (fillfactor='80') TABLESPACE foo
6767
(3 rows)
6868

69+
-- Test that a tablespace is quoted as an identifier
70+
SELECT regexp_replace(
71+
repack.repack_indexdef(indexrelid, 'testts1'::regclass, 'foo bar', false),
72+
'_[0-9]+', '_OID', 'g')
73+
FROM pg_index i join pg_class c ON c.oid = indexrelid
74+
WHERE indrelid = 'testts1'::regclass ORDER BY relname;
75+
regexp_replace
76+
---------------------------------------------------------------------------------------------------------
77+
CREATE INDEX index_OID ON repack.table_OID USING btree (id) TABLESPACE "foo bar" WHERE (id > 0)
78+
CREATE UNIQUE INDEX index_OID ON repack.table_OID USING btree (id) TABLESPACE "foo bar"
79+
CREATE INDEX index_OID ON repack.table_OID USING btree (id) WITH (fillfactor='80') TABLESPACE "foo bar"
80+
(3 rows)
81+
6982
-- can move the tablespace from default
7083
\! pg_repack --dbname=contrib_regression --no-order --table=testts1 --tablespace testts
7184
INFO: repacking table "public.testts1"

regress/expected/trigger.out

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--
2+
-- repack.repack_trigger tests
3+
--
4+
CREATE TABLE trigger_t1 (a int, b int, primary key (a, b));
5+
CREATE INDEX trigger_t1_idx ON trigger_t1 (a, b);
6+
SELECT create_trigger FROM repack.tables WHERE relname = 'public.trigger_t1';
7+
create_trigger
8+
----------------------------------------------------------------------------------------------------------------------------------------------------
9+
CREATE TRIGGER repack_trigger AFTER INSERT OR DELETE OR UPDATE ON public.trigger_t1 FOR EACH ROW EXECUTE PROCEDURE repack.repack_trigger('a', 'b')
10+
(1 row)
11+
12+
SELECT oid AS t1_oid FROM pg_catalog.pg_class WHERE relname = 'trigger_t1'
13+
\gset
14+
CREATE TYPE repack.pk_:t1_oid AS (a integer, b integer);
15+
CREATE TABLE repack.log_:t1_oid (id bigserial PRIMARY KEY, pk repack.pk_:t1_oid, row public.trigger_t1);
16+
CREATE TRIGGER repack_trigger AFTER INSERT OR DELETE OR UPDATE ON trigger_t1
17+
FOR EACH ROW EXECUTE PROCEDURE repack.repack_trigger('a', 'b');
18+
INSERT INTO trigger_t1 VALUES (111, 222);
19+
UPDATE trigger_t1 SET a=333, b=444 WHERE a = 111;
20+
DELETE FROM trigger_t1 WHERE a = 333;
21+
SELECT * FROM repack.log_:t1_oid;
22+
id | pk | row
23+
----+-----------+-----------
24+
1 | | (111,222)
25+
2 | (111,222) | (333,444)
26+
3 | (333,444) |
27+
(3 rows)
28+

0 commit comments

Comments
 (0)