-
Notifications
You must be signed in to change notification settings - Fork 1k
sql: rewrite scid string literals to use scid() for index support #9430
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
cecc0ab
996dc6e
763282b
8afb352
3a00898
8d3a5fb
cc587d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -70,7 +70,7 @@ static const struct fieldtypemap fieldtypemap[] = { | |
| { "boolean", "INTEGER" }, /* FIELD_BOOL */ | ||
| { "number", "REAL" }, /* FIELD_NUMBER */ | ||
| { "string", "TEXT" }, /* FIELD_STRING */ | ||
| { "short_channel_id", "TEXT" }, /* FIELD_SCID */ | ||
| { "short_channel_id", "SCID" }, /* FIELD_SCID */ | ||
| { "outpoint", "TEXT" }, /* FIELD_OUTPOINT */ | ||
| }; | ||
|
|
||
|
|
@@ -245,6 +245,52 @@ static enum fieldtype find_fieldtype(const jsmntok_t *name) | |
| name->end - name->start, schemas + name->start); | ||
| } | ||
|
|
||
| /* SQLite custom function: scid('NNNxNNNxNNN') -> u64 integer. | ||
| * Allows efficient queries like: WHERE in_channel = scid('735095x480x1') */ | ||
| static void sql_scid_func(sqlite3_context *ctx, int argc, sqlite3_value **argv) | ||
| { | ||
| struct short_channel_id scid; | ||
| const char *str; | ||
|
|
||
| if (argc != 1) { | ||
| sqlite3_result_error(ctx, "scid() requires exactly one argument", -1); | ||
| return; | ||
| } | ||
| if (sqlite3_value_type(argv[0]) == SQLITE_NULL) { | ||
| sqlite3_result_null(ctx); | ||
| return; | ||
| } | ||
|
|
||
| str = (const char *)sqlite3_value_text(argv[0]); | ||
| if (!str || !short_channel_id_from_str(str, strlen(str), &scid)) { | ||
| sqlite3_result_error(ctx, "invalid short_channel_id format, expected NNNxNNNxNNN", -1); | ||
| return; | ||
| } | ||
|
|
||
| sqlite3_result_int64(ctx, scid.u64); | ||
| } | ||
|
|
||
| /* SQLite custom function: fmt_scid(u64) -> 'NNNxNNNxNNN' string. | ||
| * Useful for displaying integer SCIDs in text format within SQL expressions. */ | ||
| static void sql_fmt_scid_func(sqlite3_context *ctx, int argc, sqlite3_value **argv) | ||
| { | ||
| struct short_channel_id scid; | ||
| char *str; | ||
|
|
||
| if (argc != 1) { | ||
| sqlite3_result_error(ctx, "fmt_scid() requires exactly one argument", -1); | ||
| return; | ||
| } | ||
| if (sqlite3_value_type(argv[0]) == SQLITE_NULL) { | ||
| sqlite3_result_null(ctx); | ||
| return; | ||
| } | ||
|
|
||
| scid.u64 = sqlite3_value_int64(argv[0]); | ||
| str = fmt_short_channel_id(tmpctx, scid); | ||
| sqlite3_result_text(ctx, str, -1, SQLITE_TRANSIENT); | ||
| } | ||
|
|
||
| static struct sqlite3 *sqlite_setup(struct plugin *plugin) | ||
| { | ||
| int err; | ||
|
|
@@ -301,6 +347,12 @@ static struct sqlite3 *sqlite_setup(struct plugin *plugin) | |
| plugin_err(plugin, "Could not disable sync: %s", errmsg); | ||
| } | ||
|
|
||
| /* Register custom SCID functions for integer<->text conversion */ | ||
| sqlite3_create_function(db, "scid", 1, SQLITE_UTF8, NULL, | ||
| sql_scid_func, NULL, NULL); | ||
| sqlite3_create_function(db, "fmt_scid", 1, SQLITE_UTF8, NULL, | ||
| sql_fmt_scid_func, NULL, NULL); | ||
|
|
||
| return db; | ||
| } | ||
|
|
||
|
|
@@ -424,6 +476,10 @@ static int sqlite_authorize(void *dbq_, int code, | |
| return SQLITE_OK; | ||
| if (streq(b, "json_group_array")) | ||
| return SQLITE_OK; | ||
| if (streq(b, "scid")) | ||
| return SQLITE_OK; | ||
| if (streq(b, "fmt_scid")) | ||
| return SQLITE_OK; | ||
| } | ||
|
|
||
| /* See https://www.sqlite.org/c3ref/c_alter_table.html to decode these! */ | ||
|
|
@@ -447,6 +503,13 @@ static struct command_result *refresh_complete(struct command *cmd, | |
|
|
||
| num_cols = sqlite3_column_count(dbq->stmt); | ||
|
|
||
| /* decltype is fixed for the statement, not per row. */ | ||
| bool *is_scid = tal_arr(tmpctx, bool, num_cols); | ||
| for (int i = 0; i < num_cols; i++) { | ||
| const char *decltype = sqlite3_column_decltype(dbq->stmt, i); | ||
| is_scid[i] = decltype && streq(decltype, "SCID"); | ||
| } | ||
|
|
||
| /* We normally hit an error immediately, so return a simple error then */ | ||
| ret = NULL; | ||
| num_rows = 0; | ||
|
|
@@ -465,7 +528,14 @@ static struct command_result *refresh_complete(struct command *cmd, | |
| switch (sqlite3_column_type(dbq->stmt, i)) { | ||
| case SQLITE_INTEGER: { | ||
| s64 v = sqlite3_column_int64(dbq->stmt, i); | ||
| json_add_s64(ret, NULL, v); | ||
| if (is_scid[i]) { | ||
| struct short_channel_id scid; | ||
| scid.u64 = (u64)v; | ||
| json_add_string(ret, NULL, | ||
| fmt_short_channel_id(tmpctx, scid)); | ||
| } else { | ||
| json_add_s64(ret, NULL, v); | ||
| } | ||
| break; | ||
| } | ||
| case SQLITE_FLOAT: { | ||
|
|
@@ -790,7 +860,18 @@ static struct command_result *process_json_obj(struct command *cmd, | |
| } | ||
| sqlite3_bind_int64(stmt, (*sqloff)++, valmsat.millisatoshis /* Raw: db */); | ||
| break; | ||
| case FIELD_SCID: | ||
| case FIELD_SCID: { | ||
| struct short_channel_id scid; | ||
| if (!json_to_short_channel_id(buf, coltok, &scid)) { | ||
| return command_fail(cmd, LIGHTNINGD, | ||
| "column %zu row %zu not a valid short_channel_id: %.*s", | ||
| i, row, | ||
| json_tok_full_len(coltok), | ||
| json_tok_full(buf, coltok)); | ||
| } | ||
| sqlite3_bind_int64(stmt, (*sqloff)++, scid.u64); | ||
| break; | ||
| } | ||
| case FIELD_STRING: | ||
| case FIELD_OUTPOINT: | ||
| sqlite3_bind_text(stmt, (*sqloff)++, buf + coltok->start, | ||
|
|
@@ -1029,8 +1110,8 @@ static void delete_channel_from_db(struct command *cmd, | |
| err = sqlite3_exec(sql->db, | ||
| tal_fmt(tmpctx, | ||
| "DELETE FROM channels" | ||
| " WHERE short_channel_id = '%s'", | ||
| fmt_short_channel_id(tmpctx, scid)), | ||
| " WHERE short_channel_id = %"PRIu64, | ||
| scid.u64), | ||
| NULL, NULL, &errmsg); | ||
| if (err != SQLITE_OK) | ||
| plugin_err(cmd->plugin, "Could not delete from channels: %s", | ||
|
|
@@ -1404,6 +1485,72 @@ static struct command_result *refresh_tables(struct command *cmd, | |
| return td->refresh(cmd, dbq->tables[0], dbq); | ||
| } | ||
|
|
||
| /* Check if a string is a valid short_channel_id (NNNxNNNxNNN format) */ | ||
| static bool looks_like_scid(const char *str, size_t len) | ||
| { | ||
| struct short_channel_id scid; | ||
|
|
||
| return short_channel_id_from_str(str, len, &scid); | ||
| } | ||
|
|
||
| /* Copy [start, end) onto result in one append. */ | ||
| static void append_span(char **result, const char *start, const char *end) | ||
| { | ||
| if (end > start) | ||
| tal_append_fmt(result, "%.*s", (int)(end - start), start); | ||
| } | ||
|
|
||
| /* Rewrite SQL query to wrap scid string literals with scid() function. | ||
| * This transforms '735095x480x1' into scid('735095x480x1') so that | ||
| * SQLite can use indexes on integer SCID columns. */ | ||
| static const char *rewrite_scid_literals(const tal_t *ctx, const char *query) | ||
| { | ||
| char *result = tal_strdup(ctx, ""); | ||
| const char *p = query; | ||
| const char *span = query; | ||
|
|
||
| while (*p) { | ||
| const char *start, *end; | ||
|
|
||
| /* Copy the non-quoted span in one append, not a byte at a time. */ | ||
| if (*p != '\'') { | ||
| p++; | ||
| continue; | ||
| } | ||
| append_span(&result, span, p); | ||
| span = p; | ||
|
|
||
| start = p + 1; | ||
| end = strchr(start, '\''); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if (!end) { | ||
| /* Unterminated quote, copy the rest and stop. */ | ||
| tal_append_fmt(&result, "%s", p); | ||
| return result; | ||
| } | ||
|
|
||
| if (looks_like_scid(start, end - start)) { | ||
| /* Already wrapped: scid('735095x480x1'). Leave it. */ | ||
| bool already_wrapped = false; | ||
| if (p - query >= 5 && strncmp(p - 5, "scid(", 5) == 0) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in |
||
| already_wrapped = true; | ||
| if (!already_wrapped) { | ||
| tal_append_fmt(&result, "scid('%.*s')", | ||
| (int)(end - start), start); | ||
| p = end + 1; | ||
| span = p; | ||
| continue; | ||
| } | ||
| } | ||
| /* Not a scid, or already wrapped: keep the quoted literal. */ | ||
| p = end + 1; | ||
| append_span(&result, span, p); | ||
| span = p; | ||
| } | ||
| append_span(&result, span, p); | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| static struct command_result *json_sql(struct command *cmd, | ||
| const char *buffer, | ||
| const jsmntok_t *params) | ||
|
|
@@ -1418,6 +1565,10 @@ static struct command_result *json_sql(struct command *cmd, | |
| NULL)) | ||
| return command_param_failed(); | ||
|
|
||
| /* Rewrite scid string literals to use scid() function so | ||
| * SQLite can use indexes on integer SCID columns. */ | ||
| query = rewrite_scid_literals(tmpctx, query); | ||
|
|
||
| dbq->tables = tal_arr(dbq, struct table_desc *, 0); | ||
| dbq->authfail = NULL; | ||
| dbq->cmd = cmd; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rewrite_scid_literals()does not know which column a literal is compared with. SoWHERE label = '100x2x3'oninvoicesbecomeslabel = scid('100x2x3'). That compares text with an integer, and the query silently returns nothing. The same happens withdescription,SELECT '1x2x3'andjson_object('k', '1x2x3')it gets worse because
short_channel_id_from_str()usessscanf("%ux%ux%hu")and never checks for trailing characters (bitcoin/short_channel_id.c at line 51). So'100x2x3-backup'also counts as an scid, gets rewritten, and is truncated to100x2x3maaaaybe we should not rewrite sql text, we can keep TEXT storage, or store integers, but document
scid()as the way to filter, and do not rewrite queries. or require the whole literal to match the scid format exactly, with no trailing characters