Skip to content

Instantly share code, notes, and snippets.

@greggirwin
Last active October 8, 2025 18:09
Show Gist options
  • Save greggirwin/d990fff8cf55d2aaa53bd23a0e47acde to your computer and use it in GitHub Desktop.
Save greggirwin/d990fff8cf55d2aaa53bd23a0e47acde to your computer and use it in GitHub Desktop.
Red [
title: "A simple flat-block db model."
author: "Gregg Irwin"
url: https://gist.github.com/greggirwin/d990fff8cf55d2aaa53bd23a0e47acde
notes: {
Mocked up in response to chat that came up based on `remove/key`
being case sensitive, and how Reducers can/should use flat blocks.
https://github.com/red/red/issues/5652
A key question is how much native funcs like `remove` and `put`
should be extended to support use with flat blocks, or whether
we should roll mezzanine wrappers which add more value, like
enforcing checks on the record size and storing the record size
in a way that it can be found, rather than using magic numbers
in code, even if behind var names.
This is about as basic as it gets, but adding specs for the schema
would be very easy to do, and that metadata would also be stored
in the DB object.
Flat blocks are more efficient, as every record doesn't have its
own container (block/map/object) that add overhead. They are simply
values. The big problem is that, without metadata, they are opaque,
harder to maintain over time, and more prone to error. This mockup
does NOT address *field* naming access, but that could also be
schema driven.
It also is NOT specifically a key-value store, which means there is
one (compound) value associated with each key.
With a simple metadata/spec addition, much can be driven by that,
and that metata can...should...MUST be included with the flat data
itself.
}
]
;-------------------------------------------------------------------------------
flat-block-rec-support-ctx: context [
make-rec-func: function [
"Make a function using a standard [db-object key] spec."
name [word! block!] "Function name (will be set)"
doc-str [string!] "Function doc string"
body [block!] "Function body"
/with-rec "Include a rec arg in the spec, for add/put"
][
set name function compose [ ; set the name in the global context
(doc-str) ; compose the doc string into the spec
db [object!]
key
(either with-rec [[rec [block!] "Append as flat values."]][])
/case "Perform a case-sensitive search."
/same {Use "same?" as comparator.}
] body ; use the body directly
name ; return the name
]
make-rec-funcs: func [
"Make a group of [series key] functions; returns func names set"
spec [block!] "[name doc-string body] triples"
/with-rec "Include a rec arg in the spec, for add/put"
][
collect [
foreach [name doc-str body] spec [
;keep make-rec-func name doc-str body
keep make-rec-func/:with-rec name doc-str body
]
]
]
; The idea here is that these all have the same spec and short bodies,
; so we can eliminate a lot of boilerplate.
; Read access funcs
specs: [
find-rec
{Returns the series where the key is found, or NONE if the key doesn't exist.}
[find/only/skip/:case/:same db/data :key db/rec-size]
has-rec?
{Returns true if key exists; false otherwise}
[not none? find-rec/:case/:same db :key]
get-rec
{Returns the record associated with a key, or none}
[if pos: find-rec/:case/:same db :key [new-line/all copy/part next pos db/rec-size off]]
remove-rec
{Removes the record for the given key, or NONE if the key doesn't exist.}
[remove/part find-rec/:case/:same db :key db/rec-size]
take-rec
{Removes and returns the records for the given key, or NONE if the key doesn't exist.}
[if pos: find-rec/:case/:same db :key [new-line/all take/part pos db/rec-size off]]
]
make-rec-funcs specs
; ;?? Is using the same method as other func-makers better here?
; ; The bodies are bigger, but it does ensure specs are consistent.
; Write access funcs
specs: [
put-rec
{Add or update a record.}
[
; Account for key being part of the record
if db/rec-size <> (1 + length? rec) [
do make error! "Number of fields doesn't match record size."
]
either pos: find-rec/:case/:same db :key [
head change next pos :rec
][
append/only db/data :key
append db/data :rec
]
new-line/skip db/data on db/rec-size ; just for visual sanity when testing
]
[add-rec insert-rec]
{Adds a new record; error if key already exists.}
[
if find-rec/:case/:same db :key [
do make error! rejoin ["A record with that key already exists. key: " key]
]
put-rec/:case/:same db :key rec
]
[change-rec update-rec]
{Update a record; error if key doesn't exist.}
[
if not find-rec/:case/:same db :key [
do make error! rejoin ["A record with that key does not exist. key: " key]
]
put-rec/:case/:same db :key rec
]
]
make-rec-funcs/with-rec specs
; ;?? Is using the same method as other func-makers better here?
; ; The bodies are bigger, but it does ensure specs are consistent.
; make-rec-func/with-rec
; 'put-rec
; {Add or update a record.}
; [
; ; Account for key being part of the record
; if db/rec-size <> (1 + length? rec) [
; do make error! "Number of fields doesn't match record size."
; ]
; either pos: find-rec/:case/:same db :key [
; head change next pos :rec
; ][
; append db/data :key
; append db/data :rec
; ]
; new-line/skip db/data on db/rec-size ; just for visual sanity when testing
; ]
;
; make-rec-func/with-rec
; [add-rec insert-rec]
; {Adds a new record; error if key already exists.}
; [
; if find-rec/:case/:same db :key [
; do make error! rejoin ["A record with that key already exists. key: " key]
; ]
; put-rec/:case/:same db :key rec
; ]
;
; make-rec-func/with-rec
; [change-rec update-rec]
; {Update a record; error if key doesn't exist.}
; [
; if not find-rec/:case/:same db :key [
; do make error! rejoin ["A record with that key does not exist. key: " key]
; ]
; put-rec/:case/:same db :key rec
; ]
; ; put-rec has an extra arg (rec), so has a different spec.
; set [put-rec change-rec] function [
; {Add or update a record}
; db [object!]
; key
; rec [block!] "Append as flat values."
; /case "Perform a case-sensitive search."
; /same {Use "same?" as comparator.}
; ][
; ; Account for key being part of the record
; if db/rec-size <> (1 + length? rec) [
; do make error! "Number of fields doesn't match record size."
; ]
; either pos: find-rec/:case/:same db :key [
; head change next pos :rec
; ][
; append db/data :key
; append db/data :rec
; ]
; new-line/skip db/data on db/rec-size ; just for visual sanity when testing
; ]
; ; add-rec has an extra arg (rec), so has a different spec.
; set [add-rec insert-rec] function [ ; new append
; {Adds a new record.}
; db [object!]
; key
; rec [block!] "Append as flat values."
; /case "Perform a case-sensitive search."
; /same {Use "same?" as comparator.}
; ][
; if find-rec/:case/:same db :key [
; do make error! rejoin ["A record with that key already exists. key: " key]
; ]
; put-rec/:case/:same db :key rec
; ]
; at-rec doesn't take a key, but a record number
set 'at-rec function [
db [object!]
rec-num [integer!]
][
at db/data (rec-num - 1) * db/rec-size + 1
]
]
;-------------------------------------------------------------------------------
flat-recs-db: context [
rec-size: none
data: none ; flat block of record data
]
make-flat-recs-storage: func [
spec [map!] "rec-size: Record size (number of fields/values); alloc: number of records to allocate initially."
][
; TBD ensure rec-size and alloc are > 0
make flat-recs-db [
rec-size: spec/rec-size
data: make block! spec/rec-size * any [spec/alloc 100]
]
]
;-------------------------------------------------------------------------------
expect-err: func [blk [block!] /local err][
if error? set/any 'err try blk [
print ["EXPECTED ERR:" mold err/arg1]
]
]
;-------------------------------------------------------------------------------
;!! Record key counts as part of the record size
db: make-flat-recs-storage #[rec-size: 4]
print ""
put-rec db 'rec-1 ['Alice #senior 1]
put-rec db 'rec-2 ['Bob #median 2]
put-rec db 'rec-3 ['Carol #junior 3]
print ["DB:" mold db]
print ['Has 'rec-1 tab has-rec? db 'rec-1]
print ['Has 'rec-2 tab has-rec? db 'rec-2]
print ['Has 'rec-3 tab has-rec? db 'rec-3]
print ['Has 'rec-4 tab has-rec? db 'rec-4]
print ""
print ['Get 'rec-1 tab mold get-rec db 'rec-1]
print ['Get 'rec-2 tab mold get-rec db 'rec-2]
print ['Get 'rec-3 tab mold get-rec db 'rec-3]
print ['Get 'rec-4 tab mold get-rec db 'rec-4]
print ""
print ['At 2 tab mold at-rec db 2]
print ""
print ['Take 'rec-2 tab mold take-rec db 'rec-2]
print ['Take 'rec-2 tab mold take-rec db 'rec-2]
print ["DB:" mold db]
print ['Add 'rec-2 mold add-rec db 'rec-2 ['Bob #median 2]]
expect-err [add-rec db 'rec-2 ['Bob #median 2]] ; dup key error
expect-err ['Change 'rec-4 mold change-rec db 'rec-4 ['Dan #doh! 4]] ; key doesn't exist error
print ['Remove 'rec-2 tab mold remove-rec db 'rec-2]
expect-err [add-rec db 'rec-2 ['Bob #median 2 xxx]] ; rec length wrong error
print ['Remove 'rec-2 tab mold remove-rec db 'rec-2]
print ['Add mold [rec x] mold add-rec db [rec x] ['Prof-X #super 2E10]]
print ["DB:" mold db]
print ['Get mold [rec x] tab mold get-rec db [rec x]]
halt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment