Skip to content

Commit 253768a

Browse files
sql: store short_channel_id as integer for efficient indexing
Short channel IDs were stored as TEXT strings (e.g., "735095x480x1") in SQLite, which prevented efficient use of indexes on SCID columns. Change storage to INTEGER (the u64 encoding), using a custom "SCID" column type so the result-reading code can detect these columns and format them back as "NNNxNNNxNNN" strings for backward-compatible JSON output. Add two new SQL functions: - scid('NNNxNNNxNNN') -> integer: for efficient WHERE clause filtering - fmt_scid(integer) -> 'NNNxNNNxNNN': for formatting in SQL expressions Fixes #8941
1 parent 376468f commit 253768a

5 files changed

Lines changed: 106 additions & 21 deletions

File tree

contrib/msggen/msggen/schema.json

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26381,7 +26381,8 @@
2638126381
"INTEGER",
2638226382
"BLOB",
2638326383
"TEXT",
26384-
"REAL"
26384+
"REAL",
26385+
"SCID"
2638526386
],
2638626387
"description": [
2638726388
"The SQL type of the column."
@@ -26501,15 +26502,15 @@
2650126502
},
2650226503
{
2650326504
"name": "short_channel_id",
26504-
"type": "TEXT"
26505+
"type": "SCID"
2650526506
},
2650626507
{
2650726508
"name": "alias_local",
26508-
"type": "TEXT"
26509+
"type": "SCID"
2650926510
},
2651026511
{
2651126512
"name": "alias_remote",
26512-
"type": "TEXT"
26513+
"type": "SCID"
2651326514
},
2651426515
{
2651526516
"name": "opener",
@@ -34093,9 +34094,11 @@
3409334094
" * JSON: string",
3409434095
" * sqlite3: TEXT",
3409534096
"",
34096-
"* *short_channel_id*. A short-channel-id of form 1x2x3.",
34097+
"* *short_channel_id*. A short-channel-id of form 1x2x3. Stored as an integer internally for efficient indexing.",
3409734098
" * JSON: string",
34098-
" * sqlite3: TEXT"
34099+
" * sqlite3: SCID (INTEGER affinity)",
34100+
"",
34101+
"You can use the `scid()` function to convert a short_channel_id string to its integer representation for queries, e.g. `WHERE in_channel = scid('1x2x3')`. The `fmt_scid()` function converts back to string form."
3409934102
],
3410034103
"permitted_sqlite3_functions": [
3410134104
"Writing to the database is not permitted, and limits are placed on various other query parameters.",
@@ -34124,7 +34127,9 @@
3412434127
"* total",
3412534128
"* unixepoch",
3412634129
"* json_object",
34127-
"* json_group_array"
34130+
"* json_group_array",
34131+
"* scid",
34132+
"* fmt_scid"
3412834133
],
3412934134
"tables": [
3413034135
"Note that tables which have a `created_index` field use that as the primary key (and `rowid` is an alias to this), otherwise an explicit `rowid` integer primary key is generated, whose value changes on each refresh. This field is used for related tables to refer to specific rows in their parent. (sqlite3 usually has this as an implicit column, but we make it explicit as the implicit version is not allowed to be used as a foreign key).",

doc/schemas/listsqlschemas.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@
6868
"INTEGER",
6969
"BLOB",
7070
"TEXT",
71-
"REAL"
71+
"REAL",
72+
"SCID"
7273
],
7374
"description": [
7475
"The SQL type of the column."
@@ -188,15 +189,15 @@
188189
},
189190
{
190191
"name": "short_channel_id",
191-
"type": "TEXT"
192+
"type": "SCID"
192193
},
193194
{
194195
"name": "alias_local",
195-
"type": "TEXT"
196+
"type": "SCID"
196197
},
197198
{
198199
"name": "alias_remote",
199-
"type": "TEXT"
200+
"type": "SCID"
200201
},
201202
{
202203
"name": "opener",

doc/schemas/sql-template.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,11 @@
7878
" * JSON: string",
7979
" * sqlite3: TEXT",
8080
"",
81-
"* *short_channel_id*. A short-channel-id of form 1x2x3.",
81+
"* *short_channel_id*. A short-channel-id of form 1x2x3. Stored as an integer internally for efficient indexing.",
8282
" * JSON: string",
83-
" * sqlite3: TEXT"
83+
" * sqlite3: SCID (INTEGER affinity)",
84+
"",
85+
"You can use the `scid()` function to convert a short_channel_id string to its integer representation for queries, e.g. `WHERE in_channel = scid('1x2x3')`. The `fmt_scid()` function converts back to string form."
8486
],
8587
"permitted_sqlite3_functions": [
8688
"Writing to the database is not permitted, and limits are placed on various other query parameters.",
@@ -109,7 +111,9 @@
109111
"* total",
110112
"* unixepoch",
111113
"* json_object",
112-
"* json_group_array"
114+
"* json_group_array",
115+
"* scid",
116+
"* fmt_scid"
113117
],
114118
"tables": [
115119
"Note that tables which have a `created_index` field use that as the primary key (and `rowid` is an alias to this), otherwise an explicit `rowid` integer primary key is generated, whose value changes on each refresh. This field is used for related tables to refer to specific rows in their parent. (sqlite3 usually has this as an implicit column, but we make it explicit as the implicit version is not allowed to be used as a foreign key).",

plugins/sql.c

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ static const struct fieldtypemap fieldtypemap[] = {
7171
{ "boolean", "INTEGER" }, /* FIELD_BOOL */
7272
{ "number", "REAL" }, /* FIELD_NUMBER */
7373
{ "string", "TEXT" }, /* FIELD_STRING */
74-
{ "short_channel_id", "TEXT" }, /* FIELD_SCID */
74+
{ "short_channel_id", "SCID" }, /* FIELD_SCID */
7575
{ "outpoint", "TEXT" }, /* FIELD_OUTPOINT */
7676
};
7777

@@ -227,6 +227,52 @@ static enum fieldtype find_fieldtype(const jsmntok_t *name)
227227
name->end - name->start, schemas + name->start);
228228
}
229229

230+
/* SQLite custom function: scid('NNNxNNNxNNN') -> u64 integer.
231+
* Allows efficient queries like: WHERE in_channel = scid('735095x480x1') */
232+
static void sql_scid_func(sqlite3_context *ctx, int argc, sqlite3_value **argv)
233+
{
234+
struct short_channel_id scid;
235+
const char *str;
236+
237+
if (argc != 1) {
238+
sqlite3_result_error(ctx, "scid() requires exactly one argument", -1);
239+
return;
240+
}
241+
if (sqlite3_value_type(argv[0]) == SQLITE_NULL) {
242+
sqlite3_result_null(ctx);
243+
return;
244+
}
245+
246+
str = (const char *)sqlite3_value_text(argv[0]);
247+
if (!str || !short_channel_id_from_str(str, strlen(str), &scid)) {
248+
sqlite3_result_error(ctx, "invalid short_channel_id format, expected NNNxNNNxNNN", -1);
249+
return;
250+
}
251+
252+
sqlite3_result_int64(ctx, scid.u64);
253+
}
254+
255+
/* SQLite custom function: fmt_scid(u64) -> 'NNNxNNNxNNN' string.
256+
* Useful for displaying integer SCIDs in text format within SQL expressions. */
257+
static void sql_fmt_scid_func(sqlite3_context *ctx, int argc, sqlite3_value **argv)
258+
{
259+
struct short_channel_id scid;
260+
char *str;
261+
262+
if (argc != 1) {
263+
sqlite3_result_error(ctx, "fmt_scid() requires exactly one argument", -1);
264+
return;
265+
}
266+
if (sqlite3_value_type(argv[0]) == SQLITE_NULL) {
267+
sqlite3_result_null(ctx);
268+
return;
269+
}
270+
271+
scid.u64 = sqlite3_value_int64(argv[0]);
272+
str = fmt_short_channel_id(tmpctx, scid);
273+
sqlite3_result_text(ctx, str, -1, SQLITE_TRANSIENT);
274+
}
275+
230276
static struct sqlite3 *sqlite_setup(struct plugin *plugin)
231277
{
232278
int err;
@@ -283,6 +329,12 @@ static struct sqlite3 *sqlite_setup(struct plugin *plugin)
283329
plugin_err(plugin, "Could not disable sync: %s", errmsg);
284330
}
285331

332+
/* Register custom SCID functions for integer<->text conversion */
333+
sqlite3_create_function(db, "scid", 1, SQLITE_UTF8, NULL,
334+
sql_scid_func, NULL, NULL);
335+
sqlite3_create_function(db, "fmt_scid", 1, SQLITE_UTF8, NULL,
336+
sql_fmt_scid_func, NULL, NULL);
337+
286338
return db;
287339
}
288340

@@ -406,6 +458,10 @@ static int sqlite_authorize(void *dbq_, int code,
406458
return SQLITE_OK;
407459
if (streq(b, "json_group_array"))
408460
return SQLITE_OK;
461+
if (streq(b, "scid"))
462+
return SQLITE_OK;
463+
if (streq(b, "fmt_scid"))
464+
return SQLITE_OK;
409465
}
410466

411467
/* See https://www.sqlite.org/c3ref/c_alter_table.html to decode these! */
@@ -447,7 +503,15 @@ static struct command_result *refresh_complete(struct command *cmd,
447503
switch (sqlite3_column_type(dbq->stmt, i)) {
448504
case SQLITE_INTEGER: {
449505
s64 v = sqlite3_column_int64(dbq->stmt, i);
450-
json_add_s64(ret, NULL, v);
506+
const char *decltype = sqlite3_column_decltype(dbq->stmt, i);
507+
if (decltype && streq(decltype, "SCID")) {
508+
struct short_channel_id scid;
509+
scid.u64 = (u64)v;
510+
json_add_string(ret, NULL,
511+
fmt_short_channel_id(tmpctx, scid));
512+
} else {
513+
json_add_s64(ret, NULL, v);
514+
}
451515
break;
452516
}
453517
case SQLITE_FLOAT: {
@@ -781,7 +845,18 @@ static struct command_result *process_json_obj(struct command *cmd,
781845
}
782846
sqlite3_bind_int64(stmt, (*sqloff)++, valmsat.millisatoshis /* Raw: db */);
783847
break;
784-
case FIELD_SCID:
848+
case FIELD_SCID: {
849+
struct short_channel_id scid;
850+
if (!json_to_short_channel_id(buf, coltok, &scid)) {
851+
return command_fail(cmd, LIGHTNINGD,
852+
"column %zu row %zu not a valid short_channel_id: %.*s",
853+
i, row,
854+
json_tok_full_len(coltok),
855+
json_tok_full(buf, coltok));
856+
}
857+
sqlite3_bind_int64(stmt, (*sqloff)++, scid.u64);
858+
break;
859+
}
785860
case FIELD_STRING:
786861
case FIELD_OUTPOINT:
787862
sqlite3_bind_text(stmt, (*sqloff)++, buf + coltok->start,
@@ -976,8 +1051,8 @@ static void delete_channel_from_db(struct command *cmd,
9761051
err = sqlite3_exec(sql->db,
9771052
tal_fmt(tmpctx,
9781053
"DELETE FROM channels"
979-
" WHERE short_channel_id = '%s'",
980-
fmt_short_channel_id(tmpctx, scid)),
1054+
" WHERE short_channel_id = %"PRIu64,
1055+
scid.u64),
9811056
NULL, NULL, &errmsg);
9821057
if (err != SQLITE_OK)
9831058
plugin_err(cmd->plugin, "Could not delete from channels: %s",

tests/test_plugin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4129,7 +4129,7 @@ def test_sql(node_factory, bitcoind):
41294129
'pubkey': 'BLOB',
41304130
'secret': 'BLOB',
41314131
'number': 'REAL',
4132-
'short_channel_id': 'TEXT'}
4132+
'short_channel_id': 'SCID'}
41334133

41344134
# Check schemas match
41354135
for table, schema in expected_schemas.items():
@@ -4295,7 +4295,7 @@ def test_sql(node_factory, bitcoind):
42954295
l1.rpc.pay(l3.rpc.invoice(amount_msat=1000000, label='inv1000', description='description 1000 msat')['bolt11'])
42964296

42974297
# Two channels, l1->l3 *may* have an HTLC in flight.
4298-
ret = l1.rpc.sql("SELECT json_object('peer_id', hex(pc.peer_id), 'alias', alias, 'scid', short_channel_id, 'htlcs',"
4298+
ret = l1.rpc.sql("SELECT json_object('peer_id', hex(pc.peer_id), 'alias', alias, 'scid', fmt_scid(short_channel_id), 'htlcs',"
42994299
" (SELECT json_group_array(json_object('id', hex(id), 'amount_msat', amount_msat))"
43004300
" FROM peerchannels_htlcs ph WHERE ph.row = pc.rowid)) FROM peerchannels pc JOIN nodes n"
43014301
" ON pc.peer_id = n.nodeid ORDER BY n.alias, pc.peer_id;")

0 commit comments

Comments
 (0)