Fix migration filename and named placeholder construction to work for any unicode character.

Our migration library doesn't seem to work with whitespaces.
This commit is contained in:
Sebastian Jeltsch
2026-04-08 13:54:03 +02:00
parent dcb5a7636d
commit eeb52692a8
12 changed files with 53 additions and 37 deletions
@@ -250,7 +250,10 @@ function TableSplitView(props: {
const selectedTable = createMemo(() => {
const filteredTables = filteredTablesAndViews();
// useParams returns undefined as a string.
const table = params.table === "undefined" ? undefined : params.table;
const table =
params.table === "undefined"
? undefined
: decodeURIComponent(params.table);
return pickInitiallySelectedTable(filteredTables, table);
});
+1
View File
@@ -15,6 +15,7 @@ pub(crate) mod read_queries;
pub(crate) mod read_record;
pub(crate) mod subscribe;
pub(crate) mod test_utils;
pub(crate) mod util;
pub(crate) mod write_queries;
mod error;
+7 -14
View File
@@ -10,6 +10,7 @@ use trailbase_sqlite::{NamedParams, Value};
use trailbase_sqlvalue::SqlValue;
use crate::records::RecordApi;
use crate::records::util::named_placeholder;
use crate::schema_metadata::{self, JsonColumnMetadata, TableMetadata};
#[derive(Debug, Clone, thiserror::Error)]
@@ -176,7 +177,7 @@ impl Params {
files.extend(json_files);
}
named_params.push((prefix_colon(&key).into(), param));
named_params.push((named_placeholder(&key).into(), param));
column_names.push(key);
column_indexes.push(*index);
}
@@ -223,7 +224,7 @@ impl Params {
continue;
};
named_params.push((prefix_colon(&key).into(), value.try_into()?));
named_params.push((named_placeholder(&key).into(), value.try_into()?));
column_names.push(key);
column_indexes.push(*index);
}
@@ -284,7 +285,7 @@ impl Params {
));
}
named_params.push((prefix_colon(&key).into(), param));
named_params.push((named_placeholder(&key).into(), param));
column_names.push(key);
column_indexes.push(*index);
}
@@ -348,7 +349,7 @@ impl Params {
));
}
named_params.push((prefix_colon(&key).into(), param));
named_params.push((named_placeholder(&key).into(), param));
column_names.push(key);
column_indexes.push(*index);
}
@@ -492,7 +493,7 @@ fn extract_files_from_multipart<S: ColumnAccessor>(
}
named_params.push((
prefix_colon(&column.name).into(),
named_placeholder(&column.name).into(),
Value::Text(serde_json::to_string(&file_metadata)?),
));
column_names.push(column.name.to_string());
@@ -500,7 +501,7 @@ fn extract_files_from_multipart<S: ColumnAccessor>(
}
"std.FileUploads" => {
named_params.push((
prefix_colon(&column.name).into(),
named_placeholder(&column.name).into(),
Value::Text(serde_json::to_string(&file_metadata)?),
));
column_names.push(column.name.to_string());
@@ -612,14 +613,6 @@ fn extract_params_and_files_from_json(
};
}
#[inline]
pub(crate) fn prefix_colon(s: &str) -> String {
let mut new = String::with_capacity(s.len() + 1);
new.push(':');
new.push_str(s);
return new;
}
#[cfg(test)]
mod tests {
use base64::prelude::*;
+17 -12
View File
@@ -425,15 +425,19 @@ mod test {
async fn create_test_record_api(state: &AppState, api_name: &str) {
let conn = state.conn();
let table_name = "table 😍";
conn
.execute(
format!(
r#"CREATE TABLE 'table' (
r#"CREATE TABLE '{table_name}' (
id BLOB PRIMARY KEY NOT NULL CHECK(is_uuid_v7(id)) DEFAULT(uuid_v7()),
file TEXT CHECK(jsonschema('std.FileUpload', file)),
files TEXT CHECK(jsonschema('std.FileUploads', files)),
-- Add a "keyword" column to ensure escaping is correct
[index] TEXT NOT NULL DEFAULT('')
-- Add a "keyword" column to ensure escaping is correct.
[index] TEXT NOT NULL DEFAULT(''),
-- A special char column to check more escaping.
[test 😍] TEXT NOT NULL DEFAULT('')
) STRICT"#
),
(),
@@ -447,7 +451,7 @@ mod test {
&state,
RecordApiConfig {
name: Some(api_name.to_string()),
table_name: Some("table".to_string()),
table_name: Some(table_name.to_string()),
acl_world: [
PermissionFlag::Create as i32,
PermissionFlag::Read as i32,
@@ -462,7 +466,6 @@ mod test {
.unwrap();
}
// NOTE: would ideally be in a create_record test instead.
#[tokio::test]
async fn test_empty_create_record() {
let state = test_state(None).await.unwrap();
@@ -509,13 +512,10 @@ mod test {
Path(API_NAME.to_string()),
Query(CreateRecordQuery::default()),
None,
Either::Json(
json_row_from_value(json!({
"index": column_value.to_string(),
}))
.unwrap()
.into(),
),
Either::Json(json!({
"index": column_value.to_string(),
"test 😍": column_value.to_string(),
})),
)
.await
.unwrap(),
@@ -542,6 +542,11 @@ mod test {
*map.get("index").unwrap(),
serde_json::Value::String(column_value.to_string())
);
assert_eq!(
*map.get("test 😍").unwrap(),
serde_json::Value::String(column_value.to_string())
);
}
#[tokio::test]
+4 -3
View File
@@ -14,7 +14,8 @@ use trailbase_sqlite::{Connection, NamedParams, Params as _, Value};
use crate::auth::user::User;
use crate::config::proto::{ConflictResolutionStrategy, RecordApiConfig};
use crate::constants::USER_TABLE;
use crate::records::params::{LazyParams, Params, prefix_colon};
use crate::records::params::{LazyParams, Params};
use crate::records::util::named_placeholder;
use crate::records::{Permission, RecordError};
#[derive(Clone)]
@@ -70,7 +71,7 @@ impl RecordApiSchema {
.iter()
.map(|meta| {
(
Cow::Owned(prefix_colon(&meta.column.name)),
Cow::Owned(named_placeholder(&meta.column.name)),
trailbase_sqlite::Value::Null,
)
})
@@ -729,7 +730,7 @@ struct SubscriptionAclParams<'a> {
impl<'a> trailbase_sqlite::Params for SubscriptionAclParams<'a> {
fn bind(self, stmt: &mut rusqlite::Statement<'_>) -> rusqlite::Result<()> {
for (name, v) in self.params {
if let Some(idx) = stmt.parameter_index(&prefix_colon(name))? {
if let Some(idx) = stmt.parameter_index(&named_placeholder(name))? {
stmt.raw_bind_parameter(idx, v)?;
};
}
+9
View File
@@ -0,0 +1,9 @@
#[inline]
pub(crate) fn named_placeholder(s: &str) -> String {
let mut new = String::with_capacity(s.len() + 1);
new.push(':');
for char in s.chars() {
new.push(if char.is_alphanumeric() { char } else { '_' });
}
return new;
}
@@ -6,7 +6,7 @@ FROM
{% if !column_names.is_empty() -%}
, (SELECT
{%- for name in column_names -%}
{% if !loop.first %},{% endif %} :{{ name }} AS "{{ name }}"
{% if !loop.first %},{% endif %} {{ crate::records::util::named_placeholder(name) }} AS "{{ name }}"
{%- endfor -%}
) AS _REQ_
{%- endif %}
@@ -6,7 +6,7 @@ INSERT {{ conflict_clause }} INTO {{ table_name }}
{%- endfor -%}
) VALUES (
{%- for name in column_names -%}
{%- if !loop.first %},{% endif %}:{{ name }}
{%- if !loop.first %},{% endif %}{{ crate::records::util::named_placeholder(name) }}
{%- endfor -%}
)
{%- endif -%}
@@ -5,7 +5,7 @@ FROM
{% if !column_names.is_empty() -%}
, (SELECT
{%- for name in column_names -%}
{% if !loop.first %},{% endif %} :{{ name }} AS "{{ name }}"
{% if !loop.first %},{% endif %} {{ crate::records::util::named_placeholder(name) }} AS "{{ name }}"
{%- endfor -%}
) AS _ROW_
{%- endif %}
@@ -7,7 +7,7 @@ FROM
{% if !column_names.is_empty() -%}
, (SELECT
{%- for name in column_names -%}
{% if !loop.first %},{% endif %} :{{ name }} AS "{{ name }}"
{% if !loop.first %},{% endif %} {{ crate::records::util::named_placeholder(name) }} AS "{{ name }}"
{%- endfor -%}
) AS _REQ_
{%- endif %}
@@ -1,6 +1,6 @@
UPDATE {{ table_name }} SET
{%- for name in column_names -%}
{%- if !loop.first %},{% endif %} "{{ name }}" = :{{ name }}
{%- if !loop.first %},{% endif %} "{{ name }}" = {{ crate::records::util::named_placeholder(name) }}
{%- endfor %}
WHERE "{{ pk_column_name }}" = :__pk_value
{%- match returning -%}
+6 -2
View File
@@ -551,10 +551,14 @@ impl QualifiedName {
}
pub fn migration_filename(&self, prefix: &str) -> String {
fn sanitize(s: &str) -> String {
return s.replace(|c: char| !c.is_alphanumeric(), "_");
}
return if let Some(ref db) = self.database_schema {
format!("{prefix}_{db}_{}", self.name)
format!("{prefix}_{db}_{}", sanitize(&self.name), db = sanitize(db))
} else {
format!("{prefix}_{}", self.name)
format!("{prefix}_{}", sanitize(&self.name))
};
}
}