永仁留下多次案底,並曾經被建明逮捕,但也逐漸取得黑社會的信任。建明畢業後則由警員(PC
)做起,表現優異,獲面試晉陞見習督察(PI
)的機會。兩人的路就像黑白顛倒一般,誰是好人,誰又是壞人呢?
insert
此場景時間1994年insert FuzzyTime {fuzzy_year:= 1994};
insert
地標警察局insert Landmark {name:= "警察局"};
alias
及編寫測試alias
的function
建立一個year_1994
(1994年)及police_station
(警察局)的alias
。
alias year_1994:= assert_exists(assert_single((select FuzzyTime
filter .fuzzy_year = 1994
and .fuzzy_month ?= <FuzzyMonth>{}
and .fuzzy_day ?= <FuzzyDay>{}
and .fuzzy_hour ?= <FuzzyHour>{}
and .fuzzy_minute ?= <FuzzyMinute>{}
and .fuzzy_second ?= <FuzzySecond>{}
and .fuzzy_dow ?= <DayOfWeek>{}
))
);
alias police_station:= assert_exists(
assert_single(
(select Landmark filter .name="警察局")
)
);
新增test_scene03_alias()
並更新test_alias()
。
function test_alias() -> bool
using (all({
test_scene01_alias(),
test_scene02_alias(),
test_scene03_alias(),
})
);
function test_scene03_alias() -> bool
using (all({
(exists year_1994),
(exists police_station),
})
);
did you create alias 'default::police_station'? [y,n,l,c,b,s,q,?]
> y
did you create alias 'default::year_1994'? [y,n,l,c,b,s,q,?]
> y
did you create function 'default::test_scene03_alias'? [y,n,l,c,b,s,q,?]
> y
did you alter function 'default::test_alias'? [y,n,l,c,b,s,q,?]
> y
test_alias()
# 1st migration needs to be applied before running this query
select test_alias();
update
lau
假設建明由學校畢業至1994年間,官階為PC
。
update lau set {
police_rank:= PoliceRank.PC
};
insert
ChenLauContact
insert ChenLauContact {
how:= "面對面",
detail:= "建明逮捕永仁並在警局替其做筆錄。",
`when`:= year_1994,
where:= police_station,
};
Archive
及CriminalRecord
我們需要一個object type
來記錄永仁的犯罪記錄。我們選擇建立一個abstract object type
命名為Archive
,並建立一個object type
命名為CriminalRecord
來extending
Archive
。
CriminalRecord
有四個property
及一個multi link
:
ref_no property
為必填的檔案編號,並使用constraint exclusive
,確保此編號不會重覆。code property
為必填的犯罪代碼。involved multi link
為一眾涉案人等。created_at property
為檔案建立時間。使用rewrite
在insert
時,以datetime_of_statement()
為預設值。此外,我們還加上readonly := true
的限制,這麼一來就無法修改檔案的建立時間。modified_at property
為檔案修改時間。使用rewrite
在update
時,自動以datetime_of_statement()
覆寫。abstract type Archive;
type CriminalRecord extending Archive {
required ref_no: str {
constraint exclusive;
};
required code: str;
multi involved: Character;
created_at: datetime {
readonly := true;
rewrite insert using (datetime_of_statement())
}
modified_at: datetime {
rewrite update using (datetime_of_statement())
}
}
did you create object type 'default::Archive'? [y,n,l,c,b,s,q,?]
> y
did you create object type 'default::CriminalRecord'? [y,n,l,c,b,s,q,?]
> y
insert
CriminalRecord
我們選擇使用tuple
搭配for-loop
來insert
片中出現的兩次犯罪記錄,這個模式在EdgeDB中稱為bulk inserts。
於with
區塊中,建立一個records
EdgeDBset
,裡面有兩個tuple
代表兩次犯罪記錄。tuple
的第一個元素為ref_no
,而第二個元素為code
。
接著使用for-loop
+ union
+ (insert
CriminalRecord
)的語法,來insert
兩次犯罪記錄。我們可以使用.0
來取得tuple
的第一個元素、.1
來取得tuple
的第二個元素,依此類推。
# end migration needs to be applied before running this query
with records:= {("CCR9314768", "OFFNCE: A.O.A.B.H "),
("RN992317", "CD-POD ")},
for record in records
union (insert CriminalRecord {
ref_no:= record.0,
code:= record.1,
involved:= chen,
});
或是使用named tuple
的語法:
with records:= {(ref_no:= "CCR9314768", code:= "OFFNCE: A.O.A.B.H "),
(ref_no:= "RN992317", code:= "CD-POD ")},
for record in records
union (insert CriminalRecord {
ref_no:= record.ref_no,
code:= record.code,
involved:= chen,
});
此外,由於EdgeDBset
是無序的,所以insert
的順序並不能保證。如果想要有確定的insert
順序,需要搭配array
與range_unpack
。
with records:= [("CCR9314768", "OFFNCE: A.O.A.B.H "),
("RN992317", "CD-POD ")],
record_len:= len(records),
for i in range_unpack(range(0, record_len))
union (insert CriminalRecord {
ref_no:= array_get(records, i).0,
code:= array_get(records, i).1,
ref_no:= record.ref_no,
code:= record.code,
involved:= chen,
});
其原理是array
是有序的,可以使用array_get
來索引,而range_unpack
可以有序地返回range
內的值。不過,這樣的需求應該不太常見。
select
CriminalRecord {**}
我們可以使用splat
的語法,來看看兩次insert
是否成功。
select CriminalRecord {**};
{
default::CriminalRecord {
id: 740a4c4c-bc4b-11ee-b314-afc01e783d2e,
code: 'OFFNCE: A.O.A.B.H ',
created_at: <datetime>'2024-01-26T13:04:35.046364Z',
modified_at: {},
ref_no: 'CCR9314768',
involved: {
default::PoliceSpy {
nickname: '仁哥',
name: '陳永仁',
eng_name: {},
id: db49a9aa-bc48-11ee-aae4-4fe16706e7ad,
classic_lines: {},
},
},
},
default::CriminalRecord {
id: 740a56ce-bc4b-11ee-b314-bf9055e83044,
code: 'CD-POD ',
created_at: <datetime>'2024-01-26T13:04:35.046364Z',
modified_at: {},
ref_no: 'RN992317',
involved: {
default::PoliceSpy {
nickname: '仁哥',
name: '陳永仁',
eng_name: {},
id: db49a9aa-bc48-11ee-aae4-4fe16706e7ad,
classic_lines: {},
},
},
},
}
從上述結果可以觀察到:
created_at
自動於insert
時,產生檔案建立時間了。update
,所以modified_at
為空set
。code property
時,在其後多加了許多空格,讓我們學習如何update
吧。不知道您有沒有注意到因為created_at
與modified_at
為datetime
型態,所以其是帶有timezone
資訊的(預設是UTC
)。
或許您會有衝動想要將其轉為香港當地時間,但當時香港仍是英國殖民地,或許所有犯罪記錄會以英國時區的UTC
儲存在英國伺服器。Who knows? 讓我們暫時接受這個設定,即使它可能是個美麗的錯誤。
update
CriminalRecord
for-loop
除了可以用在insert
外,也能夠用在update
。由於code property
是str
型態,經過翻找文件之後,我們發現str_trim_end
正好可以滿足需求。
for record in CriminalRecord
union (
update record
set {
code:= str_trim_end(.code)
}
);
select
CriminalRecord {**}
select CriminalRecord {**};
{
default::CriminalRecord {
id: 740a4c4c-bc4b-11ee-b314-afc01e783d2e,
code: 'OFFNCE: A.O.A.B.H',
created_at: <datetime>'2024-01-26T13:04:35.046364Z',
modified_at: <datetime>'2024-01-26T13:05:35.533826Z',
ref_no: 'CCR9314768',
involved: {
default::PoliceSpy {
nickname: '仁哥',
name: '陳永仁',
eng_name: {},
id: db49a9aa-bc48-11ee-aae4-4fe16706e7ad,
classic_lines: {},
},
},
},
default::CriminalRecord {
id: 740a56ce-bc4b-11ee-b314-bf9055e83044,
code: 'CD-POD',
created_at: <datetime>'2024-01-26T13:04:35.046364Z',
modified_at: <datetime>'2024-01-26T13:05:35.533826Z',
ref_no: 'RN992317',
involved: {
default::PoliceSpy {
nickname: '仁哥',
name: '陳永仁',
eng_name: {},
id: db49a9aa-bc48-11ee-aae4-4fe16706e7ad,
classic_lines: {},
},
},
},
}
從上述結果可以觀察到:
created_at
並沒有變動。modified_at
自動於update
時更新。str_trim_end
成功去除了code
property
後的多餘空格。backlink
假設現在我們想知道永仁有哪些犯罪記錄,但是卻不想從CriminalRecord
下手的話,backlink
是一個不錯的選擇。
由於CriminalRecord
中的involved
是個multi link
,連接了involved
及Character
。backlink
讓我們可以反向來對這種關係進行query:
[is type]
讓我們指定要尋找哪一個type
下的link
。.<link
是指[is type]
這個type
下的哪一個link
。select chen {criminal_records:= .<involved[is CriminalRecord] {**}};
上面這段query可以想成,從CriminalRecord
的involved link
中,找出跟chen
有關的CriminalRecord
,命名為criminal_records
,並使用{**}
列出結果。
{
default::PoliceSpy {
criminal_records: {
default::CriminalRecord {
id: 740a4c4c-bc4b-11ee-b314-afc01e783d2e,
code: 'OFFNCE: A.O.A.B.H',
created_at: <datetime>'2024-01-26T13:04:35.046364Z',
modified_at: <datetime>'2024-01-26T13:05:35.533826Z',
ref_no: 'CCR9314768',
involved: {
default::PoliceSpy {
nickname: '仁哥',
name: '陳永仁',
eng_name: {},
id: db49a9aa-bc48-11ee-aae4-4fe16706e7ad,
classic_lines: {},
},
},
},
default::CriminalRecord {
id: 740a56ce-bc4b-11ee-b314-bf9055e83044,
code: 'CD-POD',
created_at: <datetime>'2024-01-26T13:04:35.046364Z',
modified_at: <datetime>'2024-01-26T13:05:35.533826Z',
ref_no: 'RN992317',
involved: {
default::PoliceSpy {
nickname: '仁哥',
name: '陳永仁',
eng_name: {},
id: db49a9aa-bc48-11ee-aae4-4fe16706e7ad,
classic_lines: {},
},
},
},
},
},
}
如果想知道全部Character
的CriminalRecord
,query可以這麼寫:
select Character {
name,
criminal_records:= .<involved[is CriminalRecord] {**}
};
由於現在是對Character
進行query(不是明確的chen
了),加上name
的話會比較好辨別,否則只能看到每個Character
的criminal_records
。
{
default::GangsterBoss {name: '韓琛', criminal_records: {}},
default::Police {name: '黃志誠', criminal_records: {}},
default::PoliceSpy {
name: '陳永仁',
criminal_records: {
default::CriminalRecord {
id: 740a4c4c-bc4b-11ee-b314-afc01e783d2e,
code: 'OFFNCE: A.O.A.B.H',
created_at: <datetime>'2024-01-26T13:04:35.046364Z',
modified_at: <datetime>'2024-01-26T13:05:35.533826Z',
ref_no: 'CCR9314768',
involved: {
default::PoliceSpy {
nickname: '仁哥',
name: '陳永仁',
eng_name: {},
id: db49a9aa-bc48-11ee-aae4-4fe16706e7ad,
classic_lines: {},
},
},
},
default::CriminalRecord {
id: 740a56ce-bc4b-11ee-b314-bf9055e83044,
code: 'CD-POD',
created_at: <datetime>'2024-01-26T13:04:35.046364Z',
modified_at: <datetime>'2024-01-26T13:05:35.533826Z',
ref_no: 'RN992317',
involved: {
default::PoliceSpy {
nickname: '仁哥',
name: '陳永仁',
eng_name: {},
id: db49a9aa-bc48-11ee-aae4-4fe16706e7ad,
classic_lines: {},
},
},
},
},
},
default::GangsterSpy {name: '劉建明', criminal_records: {}},
}
insert
此場景的Scene
insert Scene {
title:= "黑白顛倒",
detail:= "永仁留下多次案底,並曾經被建明逮捕,但也逐漸取得黑社會的信任。" ++
"建明畢業後則由警員(PC)做起,表現優異,獲面試晉陞見習督察" ++
"(PI)的機會。兩人的路就像黑白顛倒一般,誰是好人,誰又是壞" ++
"人呢?",
remarks:= "1.假設此時為1994年。",
who:= {chen, lau},
`when`:= year_1994,
where:= police_station,
};
我們假設無間道內警察只會在同一個地方辦公,即police_station
這個alias
。從建明與黃sir的對話來看,大多數警察辦公場景應該是在警察總部。
假設建明於1994年仍是散仔(PC
),其於劇中識別證之更換時間為1999年7月30日,時任高級督察(SIP
),在四~五年間連升數級,這又是一個如葉校長般的人物呀。