Allow PATCH clauses to accept parameters
This commit is contained in:
parent
63d099e305
commit
4ab552a8e3
9 changed files with 55 additions and 47 deletions
|
@ -64,15 +64,19 @@ impl<'a> Document<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Data::PatchExpression(data) => {
|
Data::PatchExpression(data) => {
|
||||||
|
let data = data.compute(ctx, opt, txn, Some(&self.current)).await?;
|
||||||
self.current.to_mut().patch(ctx, opt, txn, data).await?
|
self.current.to_mut().patch(ctx, opt, txn, data).await?
|
||||||
}
|
}
|
||||||
Data::MergeExpression(data) => {
|
Data::MergeExpression(data) => {
|
||||||
|
let data = data.compute(ctx, opt, txn, Some(&self.current)).await?;
|
||||||
self.current.to_mut().merge(ctx, opt, txn, data).await?
|
self.current.to_mut().merge(ctx, opt, txn, data).await?
|
||||||
}
|
}
|
||||||
Data::ReplaceExpression(data) => {
|
Data::ReplaceExpression(data) => {
|
||||||
|
let data = data.compute(ctx, opt, txn, Some(&self.current)).await?;
|
||||||
self.current.to_mut().replace(ctx, opt, txn, data).await?
|
self.current.to_mut().replace(ctx, opt, txn, data).await?
|
||||||
}
|
}
|
||||||
Data::ContentExpression(data) => {
|
Data::ContentExpression(data) => {
|
||||||
|
let data = data.compute(ctx, opt, txn, Some(&self.current)).await?;
|
||||||
self.current.to_mut().replace(ctx, opt, txn, data).await?
|
self.current.to_mut().replace(ctx, opt, txn, data).await?
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
|
|
|
@ -20,6 +20,7 @@ impl<'a> Document<'a> {
|
||||||
self.current.to_mut().def(ctx, opt, txn, rid).await?;
|
self.current.to_mut().def(ctx, opt, txn, rid).await?;
|
||||||
// This is an INSERT statement
|
// This is an INSERT statement
|
||||||
if let Workable::Insert(v) = &self.extras {
|
if let Workable::Insert(v) = &self.extras {
|
||||||
|
let v = v.compute(ctx, opt, txn, Some(&self.current)).await?;
|
||||||
self.current.to_mut().merge(ctx, opt, txn, v).await?;
|
self.current.to_mut().merge(ctx, opt, txn, v).await?;
|
||||||
}
|
}
|
||||||
// Set default field values
|
// Set default field values
|
||||||
|
|
|
@ -105,17 +105,6 @@ impl Array {
|
||||||
_ => [self.0.remove(0).as_float(), self.0.remove(0).as_float()],
|
_ => [self.0.remove(0).as_float(), self.0.remove(0).as_float()],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_operations(&self) -> Result<Vec<Operation>, Error> {
|
|
||||||
self.iter()
|
|
||||||
.map(|v| match v {
|
|
||||||
Value::Object(v) => v.to_operation(),
|
|
||||||
_ => Err(Error::InvalidPatch {
|
|
||||||
message: String::from("Operation must be an object"),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>, Error>>()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Array {
|
impl Array {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::sql::array::{array, Array};
|
|
||||||
use crate::sql::comment::mightbespace;
|
use crate::sql::comment::mightbespace;
|
||||||
use crate::sql::comment::shouldbespace;
|
use crate::sql::comment::shouldbespace;
|
||||||
use crate::sql::common::commas;
|
use crate::sql::common::commas;
|
||||||
|
@ -16,7 +15,7 @@ use std::fmt;
|
||||||
pub enum Data {
|
pub enum Data {
|
||||||
EmptyExpression,
|
EmptyExpression,
|
||||||
SetExpression(Vec<(Idiom, Operator, Value)>),
|
SetExpression(Vec<(Idiom, Operator, Value)>),
|
||||||
PatchExpression(Array),
|
PatchExpression(Value),
|
||||||
MergeExpression(Value),
|
MergeExpression(Value),
|
||||||
ReplaceExpression(Value),
|
ReplaceExpression(Value),
|
||||||
ContentExpression(Value),
|
ContentExpression(Value),
|
||||||
|
@ -98,7 +97,7 @@ fn set(i: &str) -> IResult<&str, Data> {
|
||||||
fn patch(i: &str) -> IResult<&str, Data> {
|
fn patch(i: &str) -> IResult<&str, Data> {
|
||||||
let (i, _) = tag_no_case("PATCH")(i)?;
|
let (i, _) = tag_no_case("PATCH")(i)?;
|
||||||
let (i, _) = shouldbespace(i)?;
|
let (i, _) = shouldbespace(i)?;
|
||||||
let (i, v) = array(i)?;
|
let (i, v) = value(i)?;
|
||||||
Ok((i, Data::PatchExpression(v)))
|
Ok((i, Data::PatchExpression(v)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,14 +86,13 @@ impl Value {
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::sql::array::Array;
|
|
||||||
use crate::sql::test::Parse;
|
use crate::sql::test::Parse;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn diff_none() {
|
fn diff_none() {
|
||||||
let old = Value::parse("{ test: true, text: 'text', other: { something: true } }");
|
let old = Value::parse("{ test: true, text: 'text', other: { something: true } }");
|
||||||
let now = Value::parse("{ test: true, text: 'text', other: { something: true } }");
|
let now = Value::parse("{ test: true, text: 'text', other: { something: true } }");
|
||||||
let res = Array::parse("[]");
|
let res = Value::parse("[]");
|
||||||
assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default()));
|
assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +100,7 @@ mod tests {
|
||||||
fn diff_add() {
|
fn diff_add() {
|
||||||
let old = Value::parse("{ test: true }");
|
let old = Value::parse("{ test: true }");
|
||||||
let now = Value::parse("{ test: true, other: 'test' }");
|
let now = Value::parse("{ test: true, other: 'test' }");
|
||||||
let res = Array::parse("[{ op: 'add', path: '/other', value: 'test' }]");
|
let res = Value::parse("[{ op: 'add', path: '/other', value: 'test' }]");
|
||||||
assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default()));
|
assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +108,7 @@ mod tests {
|
||||||
fn diff_remove() {
|
fn diff_remove() {
|
||||||
let old = Value::parse("{ test: true, other: 'test' }");
|
let old = Value::parse("{ test: true, other: 'test' }");
|
||||||
let now = Value::parse("{ test: true }");
|
let now = Value::parse("{ test: true }");
|
||||||
let res = Array::parse("[{ op: 'remove', path: '/other' }]");
|
let res = Value::parse("[{ op: 'remove', path: '/other' }]");
|
||||||
assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default()));
|
assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +116,7 @@ mod tests {
|
||||||
fn diff_add_array() {
|
fn diff_add_array() {
|
||||||
let old = Value::parse("{ test: [1,2,3] }");
|
let old = Value::parse("{ test: [1,2,3] }");
|
||||||
let now = Value::parse("{ test: [1,2,3,4] }");
|
let now = Value::parse("{ test: [1,2,3,4] }");
|
||||||
let res = Array::parse("[{ op: 'add', path: '/test/3', value: 4 }]");
|
let res = Value::parse("[{ op: 'add', path: '/test/3', value: 4 }]");
|
||||||
assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default()));
|
assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +124,7 @@ mod tests {
|
||||||
fn diff_replace_embedded() {
|
fn diff_replace_embedded() {
|
||||||
let old = Value::parse("{ test: { other: 'test' } }");
|
let old = Value::parse("{ test: { other: 'test' } }");
|
||||||
let now = Value::parse("{ test: { other: false } }");
|
let now = Value::parse("{ test: { other: false } }");
|
||||||
let res = Array::parse("[{ op: 'replace', path: '/test/other', value: false }]");
|
let res = Value::parse("[{ op: 'replace', path: '/test/other', value: false }]");
|
||||||
assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default()));
|
assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +132,7 @@ mod tests {
|
||||||
fn diff_change_text() {
|
fn diff_change_text() {
|
||||||
let old = Value::parse("{ test: { other: 'test' } }");
|
let old = Value::parse("{ test: { other: 'test' } }");
|
||||||
let now = Value::parse("{ test: { other: 'text' } }");
|
let now = Value::parse("{ test: { other: 'text' } }");
|
||||||
let res = Array::parse(
|
let res = Value::parse(
|
||||||
"[{ op: 'change', path: '/test/other', value: '@@ -1,4 +1,4 @@\n te\n-s\n+x\n t\n' }]",
|
"[{ op: 'change', path: '/test/other', value: '@@ -1,4 +1,4 @@\n te\n-s\n+x\n t\n' }]",
|
||||||
);
|
);
|
||||||
assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default()));
|
assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default()));
|
||||||
|
|
|
@ -11,9 +11,9 @@ impl Value {
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
opt: &Options,
|
opt: &Options,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
val: &Value,
|
val: Value,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
match val.compute(ctx, opt, txn, Some(self)).await? {
|
match val {
|
||||||
Value::Object(v) => {
|
Value::Object(v) => {
|
||||||
for (k, v) in v {
|
for (k, v) in v {
|
||||||
self.set(ctx, opt, txn, &[Part::from(k)], v).await?;
|
self.set(ctx, opt, txn, &[Part::from(k)], v).await?;
|
||||||
|
|
|
@ -2,7 +2,6 @@ use crate::ctx::Context;
|
||||||
use crate::dbs::Options;
|
use crate::dbs::Options;
|
||||||
use crate::dbs::Transaction;
|
use crate::dbs::Transaction;
|
||||||
use crate::err::Error;
|
use crate::err::Error;
|
||||||
use crate::sql::array::Array;
|
|
||||||
use crate::sql::operation::Op;
|
use crate::sql::operation::Op;
|
||||||
use crate::sql::value::Value;
|
use crate::sql::value::Value;
|
||||||
|
|
||||||
|
@ -12,7 +11,7 @@ impl Value {
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
opt: &Options,
|
opt: &Options,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
val: &Array,
|
val: Value,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
for o in val.to_operations()?.into_iter() {
|
for o in val.to_operations()?.into_iter() {
|
||||||
match o.op {
|
match o.op {
|
||||||
|
@ -51,9 +50,9 @@ mod tests {
|
||||||
async fn patch_add_simple() {
|
async fn patch_add_simple() {
|
||||||
let (ctx, opt, txn) = mock().await;
|
let (ctx, opt, txn) = mock().await;
|
||||||
let mut val = Value::parse("{ test: { other: null, something: 123 } }");
|
let mut val = Value::parse("{ test: { other: null, something: 123 } }");
|
||||||
let ops = Array::parse("[{ op: 'add', path: '/temp', value: true }]");
|
let ops = Value::parse("[{ op: 'add', path: '/temp', value: true }]");
|
||||||
let res = Value::parse("{ test: { other: null, something: 123 }, temp: true }");
|
let res = Value::parse("{ test: { other: null, something: 123 }, temp: true }");
|
||||||
val.patch(&ctx, &opt, &txn, &ops).await.unwrap();
|
val.patch(&ctx, &opt, &txn, ops).await.unwrap();
|
||||||
assert_eq!(res, val);
|
assert_eq!(res, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,9 +60,9 @@ mod tests {
|
||||||
async fn patch_remove_simple() {
|
async fn patch_remove_simple() {
|
||||||
let (ctx, opt, txn) = mock().await;
|
let (ctx, opt, txn) = mock().await;
|
||||||
let mut val = Value::parse("{ test: { other: null, something: 123 }, temp: true }");
|
let mut val = Value::parse("{ test: { other: null, something: 123 }, temp: true }");
|
||||||
let ops = Array::parse("[{ op: 'remove', path: '/temp' }]");
|
let ops = Value::parse("[{ op: 'remove', path: '/temp' }]");
|
||||||
let res = Value::parse("{ test: { other: null, something: 123 } }");
|
let res = Value::parse("{ test: { other: null, something: 123 } }");
|
||||||
val.patch(&ctx, &opt, &txn, &ops).await.unwrap();
|
val.patch(&ctx, &opt, &txn, ops).await.unwrap();
|
||||||
assert_eq!(res, val);
|
assert_eq!(res, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,9 +70,9 @@ mod tests {
|
||||||
async fn patch_replace_simple() {
|
async fn patch_replace_simple() {
|
||||||
let (ctx, opt, txn) = mock().await;
|
let (ctx, opt, txn) = mock().await;
|
||||||
let mut val = Value::parse("{ test: { other: null, something: 123 }, temp: true }");
|
let mut val = Value::parse("{ test: { other: null, something: 123 }, temp: true }");
|
||||||
let ops = Array::parse("[{ op: 'replace', path: '/temp', value: 'text' }]");
|
let ops = Value::parse("[{ op: 'replace', path: '/temp', value: 'text' }]");
|
||||||
let res = Value::parse("{ test: { other: null, something: 123 }, temp: 'text' }");
|
let res = Value::parse("{ test: { other: null, something: 123 }, temp: 'text' }");
|
||||||
val.patch(&ctx, &opt, &txn, &ops).await.unwrap();
|
val.patch(&ctx, &opt, &txn, ops).await.unwrap();
|
||||||
assert_eq!(res, val);
|
assert_eq!(res, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,11 +80,11 @@ mod tests {
|
||||||
async fn patch_change_simple() {
|
async fn patch_change_simple() {
|
||||||
let (ctx, opt, txn) = mock().await;
|
let (ctx, opt, txn) = mock().await;
|
||||||
let mut val = Value::parse("{ test: { other: null, something: 123 }, temp: 'test' }");
|
let mut val = Value::parse("{ test: { other: null, something: 123 }, temp: 'test' }");
|
||||||
let ops = Array::parse(
|
let ops = Value::parse(
|
||||||
"[{ op: 'change', path: '/temp', value: '@@ -1,4 +1,4 @@\n te\n-s\n+x\n t\n' }]",
|
"[{ op: 'change', path: '/temp', value: '@@ -1,4 +1,4 @@\n te\n-s\n+x\n t\n' }]",
|
||||||
);
|
);
|
||||||
let res = Value::parse("{ test: { other: null, something: 123 }, temp: 'text' }");
|
let res = Value::parse("{ test: { other: null, something: 123 }, temp: 'text' }");
|
||||||
val.patch(&ctx, &opt, &txn, &ops).await.unwrap();
|
val.patch(&ctx, &opt, &txn, ops).await.unwrap();
|
||||||
assert_eq!(res, val);
|
assert_eq!(res, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,9 +92,9 @@ mod tests {
|
||||||
async fn patch_add_embedded() {
|
async fn patch_add_embedded() {
|
||||||
let (ctx, opt, txn) = mock().await;
|
let (ctx, opt, txn) = mock().await;
|
||||||
let mut val = Value::parse("{ test: { other: null, something: 123 } }");
|
let mut val = Value::parse("{ test: { other: null, something: 123 } }");
|
||||||
let ops = Array::parse("[{ op: 'add', path: '/temp/test', value: true }]");
|
let ops = Value::parse("[{ op: 'add', path: '/temp/test', value: true }]");
|
||||||
let res = Value::parse("{ test: { other: null, something: 123 }, temp: { test: true } }");
|
let res = Value::parse("{ test: { other: null, something: 123 }, temp: { test: true } }");
|
||||||
val.patch(&ctx, &opt, &txn, &ops).await.unwrap();
|
val.patch(&ctx, &opt, &txn, ops).await.unwrap();
|
||||||
assert_eq!(res, val);
|
assert_eq!(res, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,9 +102,9 @@ mod tests {
|
||||||
async fn patch_remove_embedded() {
|
async fn patch_remove_embedded() {
|
||||||
let (ctx, opt, txn) = mock().await;
|
let (ctx, opt, txn) = mock().await;
|
||||||
let mut val = Value::parse("{ test: { other: null, something: 123 }, temp: true }");
|
let mut val = Value::parse("{ test: { other: null, something: 123 }, temp: true }");
|
||||||
let ops = Array::parse("[{ op: 'remove', path: '/test/other' }]");
|
let ops = Value::parse("[{ op: 'remove', path: '/test/other' }]");
|
||||||
let res = Value::parse("{ test: { something: 123 }, temp: true }");
|
let res = Value::parse("{ test: { something: 123 }, temp: true }");
|
||||||
val.patch(&ctx, &opt, &txn, &ops).await.unwrap();
|
val.patch(&ctx, &opt, &txn, ops).await.unwrap();
|
||||||
assert_eq!(res, val);
|
assert_eq!(res, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,9 +112,9 @@ mod tests {
|
||||||
async fn patch_replace_embedded() {
|
async fn patch_replace_embedded() {
|
||||||
let (ctx, opt, txn) = mock().await;
|
let (ctx, opt, txn) = mock().await;
|
||||||
let mut val = Value::parse("{ test: { other: null, something: 123 }, temp: true }");
|
let mut val = Value::parse("{ test: { other: null, something: 123 }, temp: true }");
|
||||||
let ops = Array::parse("[{ op: 'replace', path: '/test/other', value: 'text' }]");
|
let ops = Value::parse("[{ op: 'replace', path: '/test/other', value: 'text' }]");
|
||||||
let res = Value::parse("{ test: { other: 'text', something: 123 }, temp: true }");
|
let res = Value::parse("{ test: { other: 'text', something: 123 }, temp: true }");
|
||||||
val.patch(&ctx, &opt, &txn, &ops).await.unwrap();
|
val.patch(&ctx, &opt, &txn, ops).await.unwrap();
|
||||||
assert_eq!(res, val);
|
assert_eq!(res, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,11 +122,11 @@ mod tests {
|
||||||
async fn patch_change_embedded() {
|
async fn patch_change_embedded() {
|
||||||
let (ctx, opt, txn) = mock().await;
|
let (ctx, opt, txn) = mock().await;
|
||||||
let mut val = Value::parse("{ test: { other: 'test', something: 123 }, temp: true }");
|
let mut val = Value::parse("{ test: { other: 'test', something: 123 }, temp: true }");
|
||||||
let ops = Array::parse(
|
let ops = Value::parse(
|
||||||
"[{ op: 'change', path: '/test/other', value: '@@ -1,4 +1,4 @@\n te\n-s\n+x\n t\n' }]",
|
"[{ op: 'change', path: '/test/other', value: '@@ -1,4 +1,4 @@\n te\n-s\n+x\n t\n' }]",
|
||||||
);
|
);
|
||||||
let res = Value::parse("{ test: { other: 'text', something: 123 }, temp: true }");
|
let res = Value::parse("{ test: { other: 'text', something: 123 }, temp: true }");
|
||||||
val.patch(&ctx, &opt, &txn, &ops).await.unwrap();
|
val.patch(&ctx, &opt, &txn, ops).await.unwrap();
|
||||||
assert_eq!(res, val);
|
assert_eq!(res, val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,13 +7,13 @@ use crate::sql::value::Value;
|
||||||
impl Value {
|
impl Value {
|
||||||
pub async fn replace(
|
pub async fn replace(
|
||||||
&mut self,
|
&mut self,
|
||||||
ctx: &Context<'_>,
|
_ctx: &Context<'_>,
|
||||||
opt: &Options,
|
_opt: &Options,
|
||||||
txn: &Transaction,
|
_txn: &Transaction,
|
||||||
val: &Value,
|
val: Value,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
// Clear all entries
|
// Clear all entries
|
||||||
match val.compute(ctx, opt, txn, Some(self)).await? {
|
match val {
|
||||||
Value::Object(v) => {
|
Value::Object(v) => {
|
||||||
*self = Value::from(v);
|
*self = Value::from(v);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -36,7 +36,7 @@ mod tests {
|
||||||
let mut val = Value::parse("{ test: { other: null, something: 123 } }");
|
let mut val = Value::parse("{ test: { other: null, something: 123 } }");
|
||||||
let res = Value::parse("{ other: true }");
|
let res = Value::parse("{ other: true }");
|
||||||
let obj = Value::parse("{ other: true }");
|
let obj = Value::parse("{ other: true }");
|
||||||
val.replace(&ctx, &opt, &txn, &obj).await.unwrap();
|
val.replace(&ctx, &opt, &txn, obj).await.unwrap();
|
||||||
assert_eq!(res, val);
|
assert_eq!(res, val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -708,6 +708,23 @@ impl Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_operations(&self) -> Result<Vec<Operation>, Error> {
|
||||||
|
match self {
|
||||||
|
Value::Array(v) => v
|
||||||
|
.iter()
|
||||||
|
.map(|v| match v {
|
||||||
|
Value::Object(v) => v.to_operation(),
|
||||||
|
_ => Err(Error::InvalidPatch {
|
||||||
|
message: String::from("Operation must be an object"),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, Error>>(),
|
||||||
|
_ => Err(Error::InvalidPatch {
|
||||||
|
message: String::from("Operations must be an array"),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
// Simple conversion of value
|
// Simple conversion of value
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
|
|
Loading…
Reference in a new issue