Store extra items in the database too

This commit is contained in:
Jeremy Wall 2022-12-10 09:26:02 -05:00
parent 8fd940bd00
commit d354a5db0c
13 changed files with 441 additions and 115 deletions

View File

@ -1,2 +1,19 @@
-- Add up migration script here
create unique index mealplan_lookup_index on plan_recipes (user_id, plan_date, recipe_id);
-- First we collect a safe set of the data deduped for the unique index to handle using max to select a winning count.
create temp table TEMP_plan_recipes_deduped as
select user_id, plan_date, recipe_id, max(count) as count
from plan_recipes
group by user_id, plan_date, recipe_id;
-- Then we drop the plan_recipes from the table
delete from plan_recipes;
-- Create the unique index
create unique index mealplan_lookup_index
on plan_recipes (user_id, plan_date, recipe_id);
-- And finally insert the dedeuped records back into the table before dropping the temp table.
insert into plan_recipes
select user_id, plan_date, recipe_id, count
from TEMP_plan_recipes_deduped;
drop table TEMP_plan_recipes_deduped;

View File

@ -0,0 +1,57 @@
-- Add down migration script here
drop table extra_items;
-- make a copy of of the filtered_ingredients table with only latest plan_date rows
create temp table TEMP_filtered_ingredients_copy as
select
user_id,
name,
max(plan_date) as plan_date,
form,
measure_type
from filtered_ingredients
group by user_id, name, form, measure_type;
-- Drop the filtered ingredients table and recreate without plan_date
drop table filtered_ingredients;
create table filtered_ingredients(
user_id TEXT NOT NULL,
name TEXT NOT NULL,
form TEXT NOT NULL,
measure_type TEXT NOT NULL,
primary key(user_id, name, form, measure_type)
);
-- Populate the new filtered ingredients table from the copied table
insert into filtered_ingredients
select user_id, name, form, measure_type
from TEMP_filtered_ingredients_copy;
-- make a copy of of the modified_amts table with only latest plan_date rows
create temp table TEMP_modified_amts_copy as
select
user_id,
name,
form,
measure_type,
max(plan_date) as plan_date,
amt
from modified_amts;
-- Drop modified_amts and recreate without plan_date.
drop table modified_amts;
create table modified_amts(
user_id TEXT NOT NULL,
name TEXT NOT NULL,
form TEXT NOT NULL,
measure_type TEXT NOT NULL,
amt TEXT NOT NULL,
primary key(user_id, name, form, measure_type)
);
-- Populate the new modified amts with rows from the copy.
insert into modified_amts
select user_id, name, form, measure_type, amt
from TEMP_modified_amts_copy;
drop table TEMP_modified_amts_copy;

View File

@ -0,0 +1,68 @@
-- Add up migration script here
-- Create our extra items table
create table extra_items(
user_id TEXT NOT NULL,
name TEXT NOT NULL,
plan_date DATE NOT NULL,
amt TEXT NOT NULL,
primary key(user_id, name, plan_date)
);
-- Store a copy of filtered ingredients with current date as plan_date
create temp table TEMP_filtered_ingredients_copy as
select
user_id,
name,
date() as plan_date,
form,
measure_type
from filtered_ingredients;
-- Drop the filtered ingredients table and recreate with plan_date in the primary key
drop table filtered_ingredients;
create table filtered_ingredients(
user_id TEXT NOT NULL,
name TEXT NOT NULL,
form TEXT NOT NULL,
measure_type TEXT NOT NULL,
plan_date DATE NOT NULL,
primary key(user_id, name, form, measure_type, plan_date)
);
-- Populate the new filtered ingredients table from the copied table
insert into filtered_ingredients
select user_id, name, form, measure_type, plan_date
from TEMP_filtered_ingredients_copy;
drop table TEMP_filtered_ingredients_copy;
-- make a copy of of the modified_amts table with current date as plan_date
create temp table TEMP_modified_amts_copy as
select
user_id,
name,
form,
measure_type,
date() as plan_date,
amt
from modified_amts;
-- Drop modified_amts and recreate with plan_date as part of primary key.
drop table modified_amts;
create table modified_amts(
user_id TEXT NOT NULL,
name TEXT NOT NULL,
form TEXT NOT NULL,
measure_type TEXT NOT NULL,
plan_date DATE NOT NULL,
amt TEXT NOT NULL,
primary key(user_id, name, form, measure_type, plan_date)
);
-- Populate the new modified amts with rows from the copy.
insert into modified_amts
select user_id, name, form, measure_type, plan_date, amt
from TEMP_modified_amts_copy;
drop table TEMP_modified_amts_copy;

View File

@ -1,14 +1,34 @@
{
"db": "SQLite",
"07f619ff4474e9eb5f4d56497abb724e6952b4e43d681ba5ecd61490cf990ae9": {
"04987493e4b13793a2dff75cc2710972bb28abf303275f5e6346470cdf5c2c17": {
"describe": {
"columns": [],
"nullable": [],
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "form",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "measure_type",
"ordinal": 2,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Right": 1
}
},
"query": "delete from filtered_ingredients where user_id = ?"
"query": "with latest_dates as (\n select\n user_id,\n name,\n form,\n measure_type,\n max(plan_date) as plan_date\n from filtered_ingredients\n where user_id = ?\n)\n\nselect\n filtered_ingredients.name,\n filtered_ingredients.form,\n filtered_ingredients.measure_type\nfrom latest_dates\ninner join filtered_ingredients on\n latest_dates.user_id = filtered_ingredients.user_id\n and latest_dates.name = filtered_ingredients.name\n and latest_dates.form = filtered_ingredients.form\n and latest_dates.measure_type = filtered_ingredients.measure_type\n and latest_dates.plan_date = filtered_ingredients.plan_date"
},
"104f07472670436d3eee1733578bbf0c92dc4f965d3d13f9bf4bfbc92958c5b6": {
"describe": {
@ -28,6 +48,16 @@
},
"query": "select password_hashed from users where id = ?"
},
"160a9dfccf2e91a37d81f75eba21ec73105a7453c4f1fe76a430d04e525bc6cd": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 4
}
},
"query": "insert into filtered_ingredients(user_id, name, form, measure_type, plan_date)\n values (?, ?, ?, ?, date()) on conflict(user_id, name, form, measure_type, plan_date) DO NOTHING"
},
"196e289cbd65224293c4213552160a0cdf82f924ac597810fe05102e247b809d": {
"describe": {
"columns": [
@ -82,6 +112,16 @@
},
"query": "select plan_date as \"plan_date: NaiveDate\", recipe_id, count\nfrom plan_recipes\nwhere\n user_id = ?\n and date(plan_date) > ?\norder by user_id, plan_date"
},
"3caefb86073c47b5dd5d05f639ddef2f7ed2d1fd80f224457d1ec34243cc56c7": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 3
}
},
"query": "insert into extra_items (user_id, name, plan_date, amt)\nvalues (?, ?, date(), ?)\non conflict (user_id, name, plan_date) do update set amt=excluded.amt"
},
"3fd4017569dca4fe73e97e0e2bd612027a8c1b17b0b10faabd6459f56ca1c0bb": {
"describe": {
"columns": [],
@ -92,6 +132,42 @@
},
"query": "insert into recipes (user_id, recipe_id, recipe_text) values (?, ?, ?)\n on conflict(user_id, recipe_id) do update set recipe_text=excluded.recipe_text"
},
"406aac6ac2b0084c31c29adec6fa2fb9bb925d92121305c8afbac009caf1ecc0": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "form",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "measure_type",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "amt",
"ordinal": 3,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false
],
"parameters": {
"Right": 1
}
},
"query": "with latest_dates as (\n select\n user_id,\n name,\n form,\n measure_type,\n amt,\n max(plan_date) as plan_date\n from modified_amts\n where user_id = ?\n)\n\nselect\n modified_amts.name,\n modified_amts.form,\n modified_amts.measure_type,\n modified_amts.amt\nfrom latest_dates\ninner join modified_amts on\n latest_dates.user_id = modified_amts.user_id\n and latest_dates.name = modified_amts.name\n and latest_dates.form = modified_amts.form\n and latest_dates.amt = modified_amts.amt\n and latest_dates.plan_date = modified_amts.plan_date"
},
"5d743897fb0d8fd54c3708f1b1c6e416346201faa9e28823c1ba5a421472b1fa": {
"describe": {
"columns": [],
@ -102,6 +178,16 @@
},
"query": "insert into users (id, password_hashed) values (?, ?)"
},
"6e28698330e42fd6c87ba1e6f1deb664c0d3995caa2b937ceac8c908e98aded6": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 5
}
},
"query": "insert into modified_amts(user_id, name, form, measure_type, amt, plan_date)\n values (?, ?, ?, ?, ?, date()) on conflict (user_id, name, form, measure_type, plan_date) do update set amt=excluded.amt"
},
"7578157607967a6a4c60f12408c5d9900d15b429a49681a4cae4e02d31c524ec": {
"describe": {
"columns": [],
@ -184,16 +270,6 @@
},
"query": "insert into sessions (id, session_value) values (?, ?)"
},
"9e24ed2ea4d235e3a036025a0a0b5ea685546a81d7f2469a59a2fc1fc88798dc": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 1
}
},
"query": "delete from modified_amts where user_id = ?"
},
"ad3408cd773dd8f9308255ec2800171638a1aeda9817c57fb8360f97115f8e97": {
"describe": {
"columns": [
@ -242,36 +318,6 @@
},
"query": "select category_text from categories where user_id = ?"
},
"d7d94a87b0153d1436eac0f6db820f25594e94decc8d740037c10802aa49157f": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "form",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "measure_type",
"ordinal": 2,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Right": 1
}
},
"query": "select name, form, measure_type from filtered_ingredients where user_id = ?"
},
"d84685a82585c5e4ae72c86ba1fe6e4a7241c4c3c9e948213e5849d956132bad": {
"describe": {
"columns": [],
@ -282,17 +328,7 @@
},
"query": "delete from sessions"
},
"f510d7f8dcb79907abd8b17bd52127af1699b3b79d2737c7729972d6bd5a0693": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 4
}
},
"query": "insert into filtered_ingredients(user_id, name, form, measure_type)\n values (?, ?, ?, ?) on conflict(user_id, name, form, measure_type) DO NOTHING"
},
"fc294739374d2a791214f747095e0bf9378989d1ff07d96a5431dbb208f21951": {
"f34ec23c5cc8f61f92464ecf68620150a8d4521b68b5099a0a7dac3328651880": {
"describe": {
"columns": [
{
@ -300,25 +336,13 @@
"ordinal": 0,
"type_info": "Text"
},
{
"name": "form",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "measure_type",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "amt",
"ordinal": 3,
"ordinal": 1,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false
],
@ -326,16 +350,6 @@
"Right": 1
}
},
"query": "select name, form, measure_type, amt from modified_amts where user_id = ?;"
},
"fc9d3f8ce9d0b42f34307aeb31c128cc3267e77613d6f2e170c95e83d6e361df": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 5
}
},
"query": "insert into modified_amts(user_id, name, form, measure_type, amt)\n values (?, ?, ?, ?, ?) on conflict (user_id, name, form, measure_type) do update set amt=excluded.amt"
"query": "with latest_dates as (\n select\n user_id,\n name,\n max(plan_date) as plan_date\n from extra_items\n where user_id = ?\n group by user_id, name\n)\n\nselect\n extra_items.name,\n extra_items.amt\nfrom latest_dates\ninner join extra_items on\n latest_dates.user_id = extra_items.user_id\n and latest_dates.name = extra_items.name\n and latest_dates.plan_date= extra_items.plan_date"
}
}

View File

@ -1,3 +1,4 @@
use std::collections::BTreeMap;
// Copyright 2022 Jeremy Wall
//
// Licensed under the Apache License, Version 2.0 (the "License");
@ -11,9 +12,9 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::{collections::BTreeSet, net::SocketAddr};
use axum::{
body::{boxed, Full},
@ -263,13 +264,13 @@ async fn api_save_plan(
}
}
async fn api_inventory(
async fn api_inventory_v2(
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
session: storage::UserIdFromSession,
) -> impl IntoResponse {
use storage::{UserId, UserIdFromSession::FoundUserId};
if let FoundUserId(UserId(id)) = session {
match app_store.fetch_inventory_data(id).await {
match app_store.fetch_latest_inventory_data(id).await {
Ok(tpl) => Ok(axum::Json::from(tpl)),
Err(e) => {
error!(err=?e);
@ -284,6 +285,76 @@ async fn api_inventory(
}
}
async fn api_inventory(
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
session: storage::UserIdFromSession,
) -> impl IntoResponse {
use storage::{UserId, UserIdFromSession::FoundUserId};
if let FoundUserId(UserId(id)) = session {
match app_store.fetch_latest_inventory_data(id).await {
Ok((item1, item2, _)) => Ok(axum::Json::from((item1, item2))),
Err(e) => {
error!(err=?e);
Err((StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))
}
}
} else {
Err((
StatusCode::UNAUTHORIZED,
"You must be authorized to use this API call".to_owned(),
))
}
}
async fn save_inventory_data(
app_store: Arc<storage::SqliteStore>,
id: String,
filtered_ingredients: BTreeSet<IngredientKey>,
modified_amts: BTreeMap<IngredientKey, String>,
extra_items: Vec<(String, String)>,
) -> (StatusCode, String) {
if let Err(e) = app_store
.save_inventory_data(id, filtered_ingredients, modified_amts, extra_items)
.await
{
error!(err=?e);
return (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e));
}
(
StatusCode::OK,
"Successfully saved inventory data".to_owned(),
)
}
async fn api_save_inventory_v2(
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
session: storage::UserIdFromSession,
Json((filtered_ingredients, modified_amts, extra_items)): Json<(
Vec<IngredientKey>,
Vec<(IngredientKey, String)>,
Vec<(String, String)>,
)>,
) -> impl IntoResponse {
use storage::{UserId, UserIdFromSession::FoundUserId};
if let FoundUserId(UserId(id)) = session {
let filtered_ingredients = filtered_ingredients.into_iter().collect();
let modified_amts = modified_amts.into_iter().collect();
save_inventory_data(
app_store,
id,
filtered_ingredients,
modified_amts,
extra_items,
)
.await
} else {
(
StatusCode::UNAUTHORIZED,
"You must be authorized to use this API call".to_owned(),
)
}
}
async fn api_save_inventory(
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
session: storage::UserIdFromSession,
@ -296,17 +367,14 @@ async fn api_save_inventory(
if let FoundUserId(UserId(id)) = session {
let filtered_ingredients = filtered_ingredients.into_iter().collect();
let modified_amts = modified_amts.into_iter().collect();
if let Err(e) = app_store
.save_inventory_data(id, filtered_ingredients, modified_amts)
.await
{
error!(err=?e);
return (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e));
}
(
StatusCode::OK,
"Successfully saved inventory data".to_owned(),
save_inventory_data(
app_store,
id,
filtered_ingredients,
modified_amts,
Vec::new(),
)
.await
} else {
(
StatusCode::UNAUTHORIZED,
@ -333,12 +401,13 @@ pub async fn ui_main(recipe_dir_path: PathBuf, store_path: PathBuf, listen_socke
let router = Router::new()
.route("/", get(|| async { Redirect::temporary("/ui/plan") }))
.route("/ui/*path", get(ui_static_assets))
// TODO(jwall): Cleanup the routing using nested routes
// TODO(jwall): We should use route_layer to enforce the authorization
// requirements here.
// recipes api path route
.route("/api/v1/recipes", get(api_recipes).post(api_save_recipes))
// recipe entry api path route
.route("/api/v1/recipe/:recipe_id", get(api_recipe_entry))
// TODO(jwall): We should use route_layer to enforce the authorization
// requirements here.
// mealplan api path routes
.route("/api/v1/plan", get(api_plan).post(api_save_plan))
.route("/api/v1/plan/:date", get(api_plan_since))
@ -347,6 +416,10 @@ pub async fn ui_main(recipe_dir_path: PathBuf, store_path: PathBuf, listen_socke
"/api/v1/inventory",
get(api_inventory).post(api_save_inventory),
)
.route(
"/api/v2/inventory",
get(api_inventory_v2).post(api_save_inventory_v2),
)
// categories api path route
.route(
"/api/v1/categories",

View File

@ -0,0 +1,18 @@
with latest_dates as (
select
user_id,
name,
max(plan_date) as plan_date
from extra_items
where user_id = ?
group by user_id, name
)
select
extra_items.name,
extra_items.amt
from latest_dates
inner join extra_items on
latest_dates.user_id = extra_items.user_id
and latest_dates.name = extra_items.name
and latest_dates.plan_date= extra_items.plan_date

View File

@ -0,0 +1,7 @@
select
name,
amt
from extra_items
where
user_id = ?
and plan_date = ?

View File

@ -1 +1,22 @@
select name, form, measure_type from filtered_ingredients where user_id = ?
with latest_dates as (
select
user_id,
name,
form,
measure_type,
max(plan_date) as plan_date
from filtered_ingredients
where user_id = ?
)
select
filtered_ingredients.name,
filtered_ingredients.form,
filtered_ingredients.measure_type
from latest_dates
inner join filtered_ingredients on
latest_dates.user_id = filtered_ingredients.user_id
and latest_dates.name = filtered_ingredients.name
and latest_dates.form = filtered_ingredients.form
and latest_dates.measure_type = filtered_ingredients.measure_type
and latest_dates.plan_date = filtered_ingredients.plan_date

View File

@ -1 +1,24 @@
select name, form, measure_type, amt from modified_amts where user_id = ?;
with latest_dates as (
select
user_id,
name,
form,
measure_type,
amt,
max(plan_date) as plan_date
from modified_amts
where user_id = ?
)
select
modified_amts.name,
modified_amts.form,
modified_amts.measure_type,
modified_amts.amt
from latest_dates
inner join modified_amts on
latest_dates.user_id = modified_amts.user_id
and latest_dates.name = modified_amts.name
and latest_dates.form = modified_amts.form
and latest_dates.amt = modified_amts.amt
and latest_dates.plan_date = modified_amts.plan_date

View File

@ -121,16 +121,21 @@ pub trait APIStore {
date: NaiveDate,
) -> Result<()>;
async fn fetch_inventory_data<S: AsRef<str> + Send>(
async fn fetch_latest_inventory_data<S: AsRef<str> + Send>(
&self,
user_id: S,
) -> Result<(Vec<IngredientKey>, Vec<(IngredientKey, String)>)>;
) -> Result<(
Vec<IngredientKey>,
Vec<(IngredientKey, String)>,
Vec<(String, String)>,
)>;
async fn save_inventory_data<S: AsRef<str> + Send>(
&self,
user_id: S,
filtered_ingredients: BTreeSet<IngredientKey>,
modified_amts: BTreeMap<IngredientKey, String>,
extra_items: Vec<(String, String)>,
) -> Result<()>;
}
@ -504,10 +509,15 @@ impl APIStore for SqliteStore {
Ok(Some(result))
}
async fn fetch_inventory_data<S: AsRef<str> + Send>(
// TODO(jwall): Do we need fetch for date variants of this.
async fn fetch_latest_inventory_data<S: AsRef<str> + Send>(
&self,
user_id: S,
) -> Result<(Vec<IngredientKey>, Vec<(IngredientKey, String)>)> {
) -> Result<(
Vec<IngredientKey>,
Vec<(IngredientKey, String)>,
Vec<(String, String)>,
)> {
let user_id = user_id.as_ref();
struct FilteredIngredientRow {
name: String,
@ -561,7 +571,22 @@ impl APIStore for SqliteStore {
row.amt,
));
}
Ok((filtered_ingredients, modified_amts))
pub struct ExtraItemRow {
name: String,
amt: String,
}
let extra_items_rows = sqlx::query_file_as!(
ExtraItemRow,
"src/web/storage/fetch_extra_items.sql",
user_id,
)
.fetch_all(self.pool.as_ref())
.await?;
let mut extra_items = Vec::new();
for row in extra_items_rows {
extra_items.push((row.name, row.amt));
}
Ok((filtered_ingredients, modified_amts, extra_items))
}
async fn save_inventory_data<S: AsRef<str> + Send>(
@ -569,18 +594,11 @@ impl APIStore for SqliteStore {
user_id: S,
filtered_ingredients: BTreeSet<IngredientKey>,
modified_amts: BTreeMap<IngredientKey, String>,
extra_items: Vec<(String, String)>,
) -> Result<()> {
let user_id = user_id.as_ref();
let mut transaction = self.pool.as_ref().begin().await?;
sqlx::query!(
"delete from filtered_ingredients where user_id = ?",
user_id
)
.execute(&mut transaction)
.await?;
sqlx::query!("delete from modified_amts where user_id = ?", user_id)
.execute(&mut transaction)
.await?;
// store the filtered_ingredients
for key in filtered_ingredients {
let name = key.name();
let form = key.form();
@ -595,6 +613,7 @@ impl APIStore for SqliteStore {
.execute(&mut transaction)
.await?;
}
// store the modified amts
for (key, amt) in modified_amts {
let name = key.name();
let form = key.form();
@ -611,6 +630,12 @@ impl APIStore for SqliteStore {
.execute(&mut transaction)
.await?;
}
// Store the extra items
for (name, amt) in extra_items {
sqlx::query_file!("src/web/storage/store_extra_items.sql", user_id, name, amt)
.execute(&mut transaction)
.await?;
}
transaction.commit().await?;
Ok(())
}

View File

@ -1,2 +1,2 @@
insert into filtered_ingredients(user_id, name, form, measure_type)
values (?, ?, ?, ?) on conflict(user_id, name, form, measure_type) DO NOTHING
insert into filtered_ingredients(user_id, name, form, measure_type, plan_date)
values (?, ?, ?, ?, date()) on conflict(user_id, name, form, measure_type, plan_date) DO NOTHING

View File

@ -1,2 +1,2 @@
insert into modified_amts(user_id, name, form, measure_type, amt)
values (?, ?, ?, ?, ?) on conflict (user_id, name, form, measure_type) do update set amt=excluded.amt
insert into modified_amts(user_id, name, form, measure_type, amt, plan_date)
values (?, ?, ?, ?, ?, date()) on conflict (user_id, name, form, measure_type, plan_date) do update set amt=excluded.amt

View File

@ -0,0 +1,3 @@
insert into extra_items (user_id, name, plan_date, amt)
values (?, ?, date(), ?)
on conflict (user_id, name, plan_date) do update set amt=excluded.amt