Last active
October 8, 2025 18:09
-
-
Save greggirwin/d990fff8cf55d2aaa53bd23a0e47acde to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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