Skip to content

Commit 03fafe7

Browse files
committed
Fix parse nullable column constraint and clearing of Record API form dirty state. #117
1 parent ccf3301 commit 03fafe7

File tree

6 files changed

+109
-46
lines changed

6 files changed

+109
-46
lines changed

trailbase-assets/js/admin/src/components/tables/CreateAlterTable.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,19 @@ export function CreateAlterTableForm(props: {
6464
try {
6565
const o = original();
6666
if (o !== undefined) {
67+
// Alter table
6768
const response = await alterTable({
6869
source_schema: o,
6970
target_schema: value,
7071
dry_run: dryRun,
7172
});
72-
console.debug("AlterTableResponse:", response);
73+
console.debug(`AlterTableResponse [dry: ${dryRun}]:`, response);
7374

7475
if (dryRun) {
7576
setSql(response.sql);
7677
}
7778
} else {
79+
// Create table
7880
const response = await createTable({ schema: value, dry_run: dryRun });
7981
console.debug(`CreateTableResponse [dry: ${dryRun}]:`, response);
8082

@@ -105,6 +107,7 @@ export function CreateAlterTableForm(props: {
105107
};
106108

107109
const form = createForm(() => ({
110+
onSubmit: async ({ value }) => await onSubmit(value, /*dryRun=*/ false),
108111
defaultValues:
109112
props.schema ??
110113
({
@@ -128,7 +131,6 @@ export function CreateAlterTableForm(props: {
128131
virtual_table: false,
129132
temporary: false,
130133
} as Table),
131-
onSubmit: async ({ value }) => await onSubmit(value, false),
132134
}));
133135

134136
form.useStore((state) => {
@@ -244,7 +246,7 @@ export function CreateAlterTableForm(props: {
244246
disabled={!state().canSubmit}
245247
variant="outline"
246248
onClick={() => {
247-
onSubmit(form.state.values, true).catch(
249+
onSubmit(form.state.values, /*dryRun=*/ true).catch(
248250
console.error,
249251
);
250252
}}

trailbase-assets/js/admin/src/components/tables/RecordApiSettings.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ function AddApiDialog(props: {
476476

477477
export function RecordApiSettingsForm(props: {
478478
close: () => void;
479-
markDirty: () => void;
479+
markDirty: (dirty?: boolean) => void;
480480
schema: Table | View;
481481
}) {
482482
const config = createConfigQuery();
@@ -595,7 +595,7 @@ enum Mode {
595595

596596
function IndividualRecordApiSettingsForm(props: {
597597
reset: (api?: RecordApiConfig) => void;
598-
markDirty: () => void;
598+
markDirty: (dirty?: boolean) => void;
599599
schema: Table | View;
600600
api: RecordApiConfig;
601601
mode: Mode;
@@ -618,10 +618,6 @@ function IndividualRecordApiSettingsForm(props: {
618618
return;
619619
}
620620

621-
// FIXME: On submit we either have to close the Sheet or unset dirty.
622-
// Otherwise the user will always be prompted for dirty state even
623-
// after submitting.
624-
625621
const newConfig = updateRecordApiConfig(c, value);
626622
try {
627623
await setConfig(queryClient, newConfig);
@@ -633,6 +629,7 @@ function IndividualRecordApiSettingsForm(props: {
633629
variant: "success",
634630
});
635631
props.reset(value);
632+
props.markDirty(false);
636633
} catch (err) {
637634
showToast({
638635
title: "Uncaught Error",
@@ -646,7 +643,7 @@ function IndividualRecordApiSettingsForm(props: {
646643

647644
form.useStore((state) => {
648645
if (state.isDirty && !state.isSubmitted) {
649-
props.markDirty();
646+
props.markDirty(true);
650647
}
651648
});
652649

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
22
import type { Table } from "./Table";
33

4+
/**
5+
* Request to alter `TABLE` schema.
6+
*
7+
* NOTE: The current approach of (source schema) -> (target schema) is not great. It doesn't allow
8+
* us to easily rename columns, e.g. is a column deleted, new or renamed?
9+
* We should probably switch to (source schema + operations).
10+
* Operations would be: add column, remove column, alter column.
11+
*/
412
export type AlterTableRequest = { source_schema: Table, target_schema: Table, dry_run: boolean | null, };

trailbase-core/src/admin/table/alter_table.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ use crate::app_state::AppState;
1111
use crate::config::proto::hash_config;
1212
use crate::transaction::{TransactionLog, TransactionRecorder};
1313

14+
/// Request to alter `TABLE` schema.
15+
///
16+
/// NOTE: The current approach of (source schema) -> (target schema) is not great. It doesn't allow
17+
/// us to easily rename columns, e.g. is a column deleted, new or renamed?
18+
/// We should probably switch to (source schema + operations).
19+
/// Operations would be: add column, remove column, alter column.
1420
#[derive(Clone, Debug, Deserialize, TS)]
1521
#[ts(export)]
1622
pub struct AlterTableRequest {

trailbase-core/src/schema_metadata.rs

Lines changed: 79 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -295,13 +295,72 @@ mod tests {
295295
use serde_json::json;
296296
use trailbase_schema::QualifiedName;
297297
use trailbase_schema::json_schema::{Expand, JsonSchemaMode, build_json_schema_expanded};
298+
use trailbase_schema::sqlite::{Column, ColumnDataType, ColumnOption};
298299

299300
use crate::app_state::*;
300301
use crate::config::proto::{PermissionFlag, RecordApiConfig};
301302
use crate::records::list_records::list_records_handler;
302303
use crate::records::read_record::{ReadRecordQuery, read_record_handler};
303304
use crate::records::test_utils::add_record_api_config;
304305

306+
#[tokio::test]
307+
async fn test_column_nullability() {
308+
let state = test_state(None).await.unwrap();
309+
310+
state
311+
.conn()
312+
.execute_batch(
313+
"
314+
CREATE TABLE test (
315+
id INTEGER PRIMARY KEY,
316+
a INT NOT NULL,
317+
b INT NULL,
318+
c INT
319+
) STRICT;
320+
321+
INSERT INTO test (a, b, c) VALUES (5, NULL, NULL), (6, 1, 2);
322+
",
323+
)
324+
.await
325+
.unwrap();
326+
327+
state.schema_metadata().invalidate_all().await.unwrap();
328+
329+
let test_table = state
330+
.schema_metadata()
331+
.get_table(&QualifiedName {
332+
name: "test".to_string(),
333+
database_schema: None,
334+
})
335+
.unwrap();
336+
337+
assert_eq!(4, test_table.schema.columns.len());
338+
assert_eq!(
339+
test_table.schema.columns[1],
340+
Column {
341+
name: "a".to_string(),
342+
data_type: ColumnDataType::Int,
343+
options: vec![ColumnOption::NotNull,],
344+
}
345+
);
346+
assert_eq!(
347+
test_table.schema.columns[2],
348+
Column {
349+
name: "b".to_string(),
350+
data_type: ColumnDataType::Int,
351+
options: vec![ColumnOption::Null,],
352+
}
353+
);
354+
assert_eq!(
355+
test_table.schema.columns[3],
356+
Column {
357+
name: "c".to_string(),
358+
data_type: ColumnDataType::Int,
359+
options: vec![],
360+
}
361+
);
362+
}
363+
305364
#[tokio::test]
306365
async fn test_expanded_foreign_key() {
307366
let state = test_state(None).await.unwrap();
@@ -527,33 +586,29 @@ mod tests {
527586
async fn test_expanded_with_multiple_foreign_keys() {
528587
let state = test_state(None).await.unwrap();
529588

530-
let exec = {
531-
let conn = state.conn();
532-
move |sql: &str| {
533-
let conn = conn.clone();
534-
let owned = sql.to_string();
535-
return async move { conn.execute(owned, ()).await };
536-
}
537-
};
589+
let table_name = "test_table";
590+
state
591+
.conn()
592+
.execute_batch(format!(
593+
r#"
594+
CREATE TABLE foreign_table0 (id INTEGER PRIMARY KEY) STRICT;
595+
INSERT INTO foreign_table0 (id) VALUES (1);
538596
539-
exec("CREATE TABLE foreign_table0 (id INTEGER PRIMARY KEY) STRICT")
540-
.await
541-
.unwrap();
542-
exec("CREATE TABLE foreign_table1 (id INTEGER PRIMARY KEY) STRICT")
543-
.await
544-
.unwrap();
597+
CREATE TABLE foreign_table1 (id INTEGER PRIMARY KEY) STRICT;
598+
INSERT INTO foreign_table1 (id) VALUES (1);
545599
546-
let table_name = "test_table";
547-
exec(&format!(
548-
r#"CREATE TABLE {table_name} (
600+
CREATE TABLE {table_name} (
549601
id INTEGER PRIMARY KEY,
550602
fk0 INTEGER REFERENCES foreign_table0(id),
551603
fk0_null INTEGER REFERENCES foreign_table0(id),
552604
fk1 INTEGER REFERENCES foreign_table1(id)
553-
) STRICT"#
554-
))
555-
.await
556-
.unwrap();
605+
) STRICT;
606+
607+
INSERT INTO {table_name} (id, fk0, fk0_null, fk1) VALUES (1, 1, NULL, 1);
608+
"#
609+
))
610+
.await
611+
.unwrap();
557612

558613
state.schema_metadata().invalidate_all().await.unwrap();
559614

@@ -570,19 +625,6 @@ mod tests {
570625
.await
571626
.unwrap();
572627

573-
exec("INSERT INTO foreign_table0 (id) VALUES (1);")
574-
.await
575-
.unwrap();
576-
exec("INSERT INTO foreign_table1 (id) VALUES (1);")
577-
.await
578-
.unwrap();
579-
580-
exec(&format!(
581-
"INSERT INTO {table_name} (id, fk0, fk0_null, fk1) VALUES (1, 1, NULL, 1);"
582-
))
583-
.await
584-
.unwrap();
585-
586628
// Expand none
587629
{
588630
let Json(value) = read_record_handler(
@@ -686,7 +728,9 @@ mod tests {
686728

687729
assert_eq!(expected, value);
688730

689-
exec(&format!("INSERT INTO {table_name} (id) VALUES (2);"))
731+
state
732+
.conn()
733+
.execute(format!("INSERT INTO {table_name} (id) VALUES (2)"), ())
690734
.await
691735
.unwrap();
692736

trailbase-schema/src/sqlite.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,13 @@ impl From<sqlite3_parser::ast::ColumnConstraint> for ColumnOption {
321321
on_update: fk.on_update,
322322
}
323323
}
324-
Constraint::NotNull { .. } => ColumnOption::NotNull,
324+
Constraint::NotNull {
325+
nullable,
326+
conflict_clause: _,
327+
} => match nullable {
328+
true => ColumnOption::Null,
329+
false => ColumnOption::NotNull,
330+
},
325331
Constraint::Default(expr) => {
326332
// NOTE: This is not using unquote on purpose to avoid turning "DEFAULT ''" into "DEFAULT".
327333
ColumnOption::Default(expr.to_string())

0 commit comments

Comments
 (0)