diff --git a/di/dbwrite/dbwrite.md b/di/dbwrite/dbwrite.md new file mode 100644 index 00000000..c9986da3 --- /dev/null +++ b/di/dbwrite/dbwrite.md @@ -0,0 +1,266 @@ +# di.dbwrite + +Write, sort, and attribute utilities for kdb+ processes that persist data to disk. + +--- + +## Features + +- Write in-memory tables to a date-partitioned HDB with `savedown` — enumerates syms, applies `p#` to `sym`, writes, then sorts +- Append rows to an existing partition with `appenddown` — enumerates syms and appends; sort separately when the partition is complete +- Sort on-disk table partitions by configured columns using `xasc` +- Apply kdb+ attributes (`p`, `s`, `g`, `u`) to on-disk columns after sort +- Sort and attribute behaviour driven by a CSV config file; a `default` row acts as a fallback +- A built-in `default` row in `params` provides an out-of-the-box fallback (sort by `time` ascending) when no config file is loaded +- Run `.Q.gc[]` with before/after memory logging +- All errors from sort, attribute application, and write operations are either caught-and-logged (sort, applyattr) or propagated to the caller (savedown, upsert) + +--- + +## Dependencies + +| Dependency | Key | Required | Description | +|---|---|---|---| +| `di.log` | `` `log `` | yes | Logging functions `info`, `warn`, `error` — each `{[ctx;msg] ...}` | + +The `log` dependency must be passed to `init`. The module throws if it is absent or `(::)`. + +--- + +## Sort config CSV + +`loadconfig` reads a CSV with four columns: + +| Column | Type | Description | +|---|---|---| +| `tabname` | symbol | Table name, or `` `default `` as a catch-all fallback | +| `att` | symbol | kdb+ attribute to apply: `p`, `s`, `g`, `u`, or empty for none | +| `column` | symbol | Column to sort or attribute | +| `sort` | boolean | `1b` — include in `xasc` sort key; `0b` — attribute only | + +Example `sort.csv`: + +``` +tabname,att,column,sort +trade,p,sym,1 +trade,,price,0 +quote,p,sym,1 +default,,time,1 +``` + +Sorts `trade` by `sym`, applies `p` to `sym`. Tables not listed fall back to `default` and sort by `time`. + +--- + +## Functions + +### Summary + +| Function | Description | +|---|---| +| `init[config;deps]` | Wire injected dependencies; must be called first | +| `savedown[dir;part;tabname;data]` | Write in-memory table to HDB partition, enumerate syms, apply `p#sym`, then sort | +| `appenddown[dir;part;tabname;data]` | Append rows to existing partition and enumerate syms; does not sort | +| `loadconfig[file]` | Load and validate the sort config CSV into module state | +| `sort[d]` | Sort an on-disk partition and apply attributes per config | +| `applyattr[dloc;colname;att]` | Apply a single kdb+ attribute to an on-disk column | +| `gc[]` | Run `.Q.gc[]` and log before/after memory stats | + +--- + +### `init[config;deps]` + +Wires injected dependencies into the module. Must be called before any other function. + +**Parameters** + +| Parameter | Type | Description | +|---|---|---| +| `config` | any | Accepted but unused; pass `(::)` | +| `deps` | dict | Must contain `` `log `` → `` `info`warn`error!(infofunc;warnfunc;errfunc) `` | + +**Returns** — generic null. + +Throws with a descriptive message if the `log` dependency is missing or set to `(::)`. + +```q +log:use`di.log +log.init[logconfig] +logdep:`info`warn`error!(log.info;log.warn;log.error) + +dbwrite:use`di.dbwrite +dbwrite.init[(::);(enlist`log)!enlist logdep] +``` + +--- + +### `savedown[dir;part;tabname;data]` + +Writes an in-memory table to a date-partitioned HDB. Enumerates symbol columns against the HDB sym file, applies `p#` to `sym` if present, writes the partition, then calls `sort` to sort and apply attributes per the loaded config. + +**Parameters** + +| Parameter | Type | Description | +|---|---|---| +| `dir` | hsym | HDB root directory (e.g. `` `:hdb ``) | +| `part` | date/month/int | Partition value | +| `tabname` | symbol | Table name — determines the partition subdirectory | +| `data` | table | In-memory table to write | + +**Returns** — generic null on success; throws on write failure. + +If `loadconfig` has not been called, the built-in default row applies (sort by `time` ascending). If the table has no `sym` column, enumeration and `p#` are skipped. + +```q +dbwrite.savedown[`:hdb;2024.01.02;`trade;data] +``` + +--- + +### `appenddown[dir;part;tabname;data]` + +Appends rows to an existing on-disk partition. Enumerates symbol columns then appends; does not sort. Call `sort` explicitly when the partition is complete. + +Keeping sort separate allows multiple intraday appends without the cost of re-sorting a growing partition on each call. + +**Parameters** + +| Parameter | Type | Description | +|---|---|---| +| `dir` | hsym | HDB root directory | +| `part` | date/month/int | Partition value | +| `tabname` | symbol | Table name | +| `data` | table | Rows to append | + +**Returns** — generic null on success; throws if the partition does not exist or on write failure. + +```q +/ intraday: append each batch as it arrives +dbwrite.appenddown[`:hdb;2024.01.02;`trade;batch] + +/ end-of-day: sort once when done +dbwrite.sort[(`trade;.Q.par[`:hdb;2024.01.02;`trade])] +``` + +--- + +### `loadconfig[file]` + +Loads and validates the sort configuration CSV, storing the result in module state for use by `sort`. + +**Parameters** + +| Parameter | Type | Description | +|---|---|---| +| `file` | hsym | Path to the sort config CSV; pass null (`` ` ``) to warn and reset `params` to the default row | + +**Returns** — generic null on success; throws on file/validation failure. + +Validation checks that all four required columns (`tabname`, `att`, `column`, `sort`) are present and that all `att` values are within `` ``p`s`g`u ``. Throws a descriptive error for invalid files or unreadable paths. + +Passing null warns at `warn` level and resets `params` to the default row — it does not throw. + +```q +dbwrite.loadconfig[`:config/sort.csv] +``` + +--- + +### `sort[d]` + +Sorts an on-disk table partition and applies configured attributes. + +**Parameters** + +| Parameter | Type | Description | +|---|---|---| +| `d` | symbol or list | Table name alone, or `(tabname;dir)`, or `(tabname;list of dirs)` — see below | + +`d` forms: + +| Form | Example | +|---|---| +| Symbol | `` `trade `` | +| Tabname + single dir | `` (`trade;`:hdb/2024.01.02/trade/) `` | +| Tabname + dir list | `` (`trade;`:hdb/2024.01.02/trade/ `:hdb/2024.01.03/trade/) `` | + +**Returns** — generic null on success; `()` if no sort config is found for the table. + +Config lookup order within the loaded params: +1. Rows where `tabname` matches — used directly. +2. Rows where `tabname = \`default` — used with a `warn` log. +3. No match — warns and returns `()`. + +Sort and attribute errors are caught, logged, and swallowed. + +```q +dbwrite.sort[(`trade;`:hdb/2024.01.02/trade/)] +``` + +--- + +### `applyattr[dloc;colname;att]` + +Applies a single kdb+ attribute to an on-disk column. + +**Parameters** + +| Parameter | Type | Description | +|---|---|---| +| `dloc` | hsym | On-disk partition directory (e.g. `` `:hdb/2024.01.02/trade/ ``) | +| `colname` | symbol | Column name | +| `att` | symbol | Attribute to apply: `` `p ``, `` `s ``, `` `g ``, or `` `u `` | + +**Returns** — generic null on success. + +Logs the attempt before applying. On failure, logs the error and continues — does not throw. + +```q +dbwrite.applyattr[`:hdb/2024.01.02/trade/;`sym;`p] +``` + +--- + +### `gc[]` + +Runs `.Q.gc[]` and logs before/after memory statistics. + +**Returns** — generic null. + +Emits two `info`-level log lines: memory stats before collection, and bytes recovered plus memory stats after. + +```q +dbwrite.gc[] +``` + +--- + +## Running tests + +```q +k4unit:use`di.k4unit +k4unit.moduletest`di.dbwrite +``` + +The test suite uses mock logging (no `di.log` dependency required). The mock wires up three no-op counters so log call counts can be asserted: + +```q +dbwrite:use`di.dbwrite +logcount:0 +loginfo:{[c;m] logcount::logcount+1} +logwarn:{[c;m] logcount::logcount+1} +logerr:{[c;m] logcount::logcount+1} +logdep:`info`warn`error!(loginfo;logwarn;logerr) +deps:(enlist`log)!enlist logdep +dbwrite.init[(::);deps] +``` + +Tests cover: dependency injection, `init` error on missing log dep, `savedown` write and sort, `savedown` without sym column, `appenddown` append without sort, explicit `sort` after `appenddown`, `appenddown` error on non-existent partition, `sort` with default row fallback / explicit config / `default` row fallback / no-match skip / empty input / wrong type, `loadconfig` with null file / valid file / unrecognised columns / unrecognised attributes / missing file / header-only file, `applyattr` on missing path / null column / invalid attribute / valid path, `gc` log count. + +--- + +## Exported symbols + +```q +export:([init;savedown;appenddown;sort;applyattr;loadconfig;gc]) +``` diff --git a/di/dbwrite/dbwrite.q b/di/dbwrite/dbwrite.q new file mode 100644 index 00000000..1e3fdc9c --- /dev/null +++ b/di/dbwrite/dbwrite.q @@ -0,0 +1,114 @@ +/ sort params table - default row sorts all tables by time ascending +params:([] tabname:enlist`default; att:enlist`; column:enlist`time; sort:enlist 1b); + + +/ load and validate sort.csv into .z.M.params +/ file: hsym path; null warns and resets params to default row +loadconfig:{[file] + if[not -11h=type file; + '"loadconfig: file must be a symbol, got type ",(string type file)]; + if[null file; + .z.m.logwarn[`dbwrite;"loadconfig called with no file; resetting params to default"]; + @[.z.M;`params;:;([] tabname:enlist`default; att:enlist`; column:enlist`time; sort:enlist 1b)]]; + if[not null file; + file:hsym file; + p:@[ + {.z.m.loginfo[`dbwrite;"retrieving sort settings from ",string x];("SSSB";enlist",")0:x}; + file; + {[f;e]'"failed to open ",string[f],": ",e}[file] + ]; + if[not all spcb:(spc:cols p) in `tabname`att`column`sort; + '"unrecognised columns (",(", " sv string spc where not spcb),") in ",string file]; + if[not all atb:(at:distinct p`att) in ``p`s`g`u; + '"unrecognised attribute(s): ",", " sv string at where not atb]; + @[.z.M;`params;:;p]]; + }; + +/ apply a single kdb+ attribute to an on-disk column; logs and swallows errors +applyattr:{[dloc;colname;att] + .z.m.loginfo[`dbwrite;"applying ",string[att]," attr to ",string[colname]," in ",string dloc]; + if[null colname; + .z.m.logerr[`dbwrite;"applyattr called with null column name in ",string dloc]]; + if[not null colname; + $[not att in `p`s`g`u; + .z.m.logerr[`dbwrite;"applyattr: invalid attribute ",string[att]," for ",string[colname]," in ",string dloc]; + .[{@[x;y;z#]};(dloc;colname;att); + {[dloc;colname;att;e] + .z.m.logerr[`dbwrite;"unable to apply ",string[att]," attr to ",string[colname]," in ",string[dloc],": ",e] + }[dloc;colname;att] + ]]]; + }; + +/ sort an on-disk table partition and apply attributes per sort.csv config +/ d: tabname | (tabname;dir) | (tabname;list of dirs) +sort:{[d] + $[not count d; + (); + not (type d) in -11 0 11h; + [.z.m.logerr[`dbwrite;"sort: d must be a symbol or list, got type ",(string type d)];()]; + [ + .z.m.loginfo[`dbwrite;"sorting ",(st:string t:first d)," table"]; + sp:$[count tabsp:select from .z.M.params where tabname=t; + [.z.m.loginfo[`dbwrite;"sort params found for: ",st];tabsp]; + count defsp:select from .z.M.params where tabname=`default; + [.z.m.logwarn[`dbwrite;"no sort params for: ",st,"; using defaults"];defsp]; + [.z.m.logwarn[`dbwrite;"no sort params for: ",st,"; skipping sort"];:()]]; + {[sp;dloc] + if[count sortcols:exec column from sp where sort,not null column; + .z.m.loginfo[`dbwrite;"sorting ",string[dloc]," by: ",", " sv string sortcols]; + .[xasc;(sortcols;dloc); + {[sc;dl;e] + .z.m.logerr[`dbwrite;"failed to sort ",string[dl]," by ",(", " sv string sc),": ",e] + }[sortcols;dloc]]]; + if[count attrcols:select column,att from sp where not null att; + .z.M.applyattr[dloc;;]'[attrcols`column;attrcols`att]]; + }[sp] each distinct (),last d; + .z.m.loginfo[`dbwrite;"finished sorting ",st," table"] + ]] + }; + + +/ write table to a date-partitioned hdb: enumerate syms, apply p# to sym if present, write, then sort +/ dir: hdb root (hsym); part: partition value (date/month/int); tabname: symbol; data: in-memory table +savedown:{[dir;part;tabname;data] + .z.m.loginfo[`dbwrite;"saving ",string[tabname]," partition ",string[part]," to ",string dir]; + path:` sv (.Q.par[dir;part;tabname];`); + data:.Q.en[dir;data]; + path set $[`sym in cols data;@[data;`sym;{`p#x}];data]; + sort[(tabname;path)]; + .z.m.loginfo[`dbwrite;"finished saving ",string tabname]; + }; + +/ append data to an existing on-disk partition; enumerate syms but do not sort +/ call sort separately when the partition is complete +/ dir: hdb root (hsym); part: partition value; tabname: symbol; data: in-memory table +appenddown:{[dir;part;tabname;data] + .z.m.loginfo[`dbwrite;"appending ",string[tabname]," partition ",string[part]," in ",string dir]; + path:` sv (.Q.par[dir;part;tabname];`); + if[not count @[key;path;{`$()}]; + '"appenddown: partition does not exist at ",string path]; + .[path;();,;.Q.en[dir;data]]; + .z.m.loginfo[`dbwrite;"finished appending ",string tabname]; + }; + +/ format current process memory stats as a loggable string +memstats:{[]"mem stats: ",{"; "sv "=" sv'flip(string key x;(string value x),\:" MB")}`long$.Q.w[]%1048576}; + +/ run .Q.gc[] and log before/after memory stats +gc:{ + .z.m.loginfo[`dbwrite;"starting garbage collect. ",.z.M.memstats[]]; + r:.Q.gc[]; + .z.m.loginfo[`dbwrite;"garbage collection returned ",(string `long$r%1048576),"MB. ",.z.M.memstats[]] + }; + +init:{[config;deps] + / config: unused, pass (::) + / deps: `log!(logdict) - `info`warn`error!(infofunc;warnfunc;errfunc) - required + logdict:$[99h=type deps;$[(`log in key deps) and not (::)~deps`log;deps`log;()!()];()!()]; + if[not count logdict; + '"di.dbwrite: log dependency is required; pass `info`warn`error functions - see di.log or refer to confluence documentation"; + ]; + .z.m.loginfo:logdict`info; + .z.m.logwarn:logdict`warn; + .z.m.logerr:logdict`error; + }; diff --git a/di/dbwrite/init.q b/di/dbwrite/init.q new file mode 100644 index 00000000..917d5dd6 --- /dev/null +++ b/di/dbwrite/init.q @@ -0,0 +1,6 @@ +/ dbwrite module - sort, attribute application, save-down manipulation, and GC utilities +/ used by processes that persist data to disk (rdb, wdb, tickerlogreplay) + +\l ::dbwrite.q + +export:([init;savedown;appenddown;sort;applyattr;loadconfig;gc]) \ No newline at end of file diff --git a/di/dbwrite/test.csv b/di/dbwrite/test.csv new file mode 100644 index 00000000..c742d5ff --- /dev/null +++ b/di/dbwrite/test.csv @@ -0,0 +1,150 @@ +action,ms,bytes,lang,code,repeat,minver,comment +/ Pre-test set-up: load module and mock loggers, and initialise module with mocks +before,0,0,q,dbwrite:use`di.dbwrite,1,,load di.dbwrite module +before,0,0,q,logcount:0,1,,initialise log call counter +before,0,0,q,loginfo:{[c;m] logcount::logcount+1},1,,mock info logger +before,0,0,q,logwarn:{[c;m] logcount::logcount+1},1,,mock warn logger +before,0,0,q,logerr:{[c;m] logcount::logcount+1},1,,mock error logger +before,0,0,q,"logdep:`info`warn`error!(loginfo;logwarn;logerr)",1,,build log dep dict +before,0,0,q,"deps:(enlist`log)!enlist logdep",1,,wrap in deps dict +before,0,0,q,dbwrite.init[(::);deps],1,,initialise module with mock loggers + +/ Test 1: init wires injected logger into individual loginfo/logwarn/logerr slots +true,0,0,q,logdep[`info]~.m.di.0dbwrite.loginfo,1,1,injected info function stored +true,0,0,q,logdep[`warn]~.m.di.0dbwrite.logwarn,1,1,injected warn function stored +true,0,0,q,logdep[`error]~.m.di.0dbwrite.logerr,1,1,injected error function stored + +/ Test 2: init errors when log dep not provided +fail,0,0,q,dbwrite.init[(::);(::)],1,1,init without log dep throws an error +fail,0,0,q,dbwrite.init[(::);`log!(::)],1,1,init with log set to (::) throws an error + +/ Test 3: sort uses default row when table name not in config +run,0,0,q,`:dbwrite_defp_tp/.d set `time`sym,1,,write column order file +run,0,0,q,`:dbwrite_defp_tp/time set 2024.01.01D09:00:00.000000000 2024.01.01D08:00:00.000000000,1,,write unsorted time column +run,0,0,q,`:dbwrite_defp_tp/sym set `IBM`AAPL,1,,write sym column +run,0,0,q,"dbwrite.sort[(`anytable;`:dbwrite_defp_tp/)]",1,,sort using default row +true,0,0,q,(asc exec time from get `:dbwrite_defp_tp/)~exec time from get `:dbwrite_defp_tp/,1,,time column sorted ascending by default row + +/ Test 4: loadconfig null resets params to default row +run,0,0,q,dbwrite.loadconfig[`],1,,call loadconfig with null file +true,0,0,q,.m.di.0dbwrite.params~([] tabname:enlist`default; att:enlist`; column:enlist`time; sort:enlist 1b),1,,params reset to default row + +/ Test 5: loadconfig - valid config file loaded and parsed correctly +run,0,0,q,cfgfile:`:dbwrite_test.csv,1,,temp sort config path +run,0,0,q,"cfgfile 0:(""tabname,att,column,sort"";""trade,p,sym,1"";""trade,,price,0"")",1,,write test sort config +run,0,0,q,dbwrite.loadconfig[cfgfile],1,,loadconfig with valid csv succeeds +true,0,0,q,2=count .m.di.0dbwrite.params,1,,two rows loaded into params +true,0,0,q,all `trade=exec tabname from .m.di.0dbwrite.params,1,,both rows are for trade table + +/ Test 5 cont: loadconfig - invalid and edge-case inputs +true,0,0,q,@[dbwrite.loadconfig;`:nonexistent_file_xyz;{1b}],1,,nonexistent file throws +true,0,0,q,@[dbwrite.loadconfig;42;{1b}],1,,non-symbol arg throws +run,0,0,q,badcols:`:dbwrite_badcols.csv,1,, +run,0,0,q,"badcols 0:(""wrongcol,att,column,sort"";""trade,p,sym,1"")",1,,csv with unrecognised column name +true,0,0,q,@[dbwrite.loadconfig;badcols;{1b}],1,,unrecognised column throws +run,0,0,q,badatt:`:dbwrite_badatt.csv,1,, +run,0,0,q,"badatt 0:(""tabname,att,column,sort"";""trade,z,sym,1"")",1,,csv with unrecognised attribute +true,0,0,q,@[dbwrite.loadconfig;badatt;{1b}],1,,unrecognised attribute throws +run,0,0,q,emptycfg:`:dbwrite_empty.csv,1,, +run,0,0,q,"emptycfg 0:enlist""tabname,att,column,sort""",1,,csv with header only +run,0,0,q,dbwrite.loadconfig[emptycfg],1,,loadconfig with header-only csv +true,0,0,q,0=count .m.di.0dbwrite.params,1,,params is empty after header-only csv + +/ Test 6: applyattr - error cases +run,0,0,q,logcount:0,1,,reset counter +run,0,0,q,"dbwrite.applyattr[`:nonexist_attr_dir/;`sym;`p]",1,,apply to non-existent path +true,0,0,q,2=logcount,1,,loginfo (before attempt) and logerr (on failure) each called once + +/ Test 6 cont: applyattr - null column name logs error and does not throw +run,0,0,q,`:dbwrite_attr_tp/.d set `sym`price,1,,write column order file +run,0,0,q,`:dbwrite_attr_tp/sym set `IBM`AAPL`MSFT,1,,write sym column +run,0,0,q,`:dbwrite_attr_tp/price set 200 100 300f,1,,write price column +run,0,0,q,logcount:0,1,,reset counter +run,0,0,q,"dbwrite.applyattr[`:dbwrite_attr_tp/;`;`p]",1,,apply with null column name +true,0,0,q,2=logcount,1,,loginfo and logerr both called for null column + +/ Test 6 cont: applyattr - invalid att logs error +run,0,0,q,logcount:0,1,,reset counter +run,0,0,q,"dbwrite.applyattr[`:dbwrite_attr_tp/;`sym;`z]",1,,apply with invalid attribute +true,0,0,q,2=logcount,1,,loginfo and logerr both called for invalid att + +/ Test 7: applyattr - attribute applied and logged when path exists +run,0,0,q,logcount:0,1,,reset counter +run,0,0,q,"dbwrite.applyattr[`:dbwrite_attr_tp/;`sym;`p]",1,,apply p attr to sym column +true,0,0,q,1=logcount,1,,loginfo called once (no error) +true,0,0,q,`p=attr get `:dbwrite_attr_tp/sym,1,,p attribute applied on disk + +/ Test 8: sort - skip when table not in config and no default row +run,0,0,q,dbwrite.loadconfig[cfgfile],1,,reload trade-only config (no default row) +run,0,0,q,logcount:0,1,,reset counter +true,0,0,q,()~dbwrite.sort[`no_params_table_xyz],1,,returns () when table not in config +true,0,0,q,2=logcount,1,,loginfo (sorting start) and logwarn (skip) both called +true,0,0,q,()~dbwrite.sort[()],1,,empty list returns () + +/ Test 8 cont: sort - wrong type logs error and returns () +run,0,0,q,logcount:0,1,,reset counter +true,0,0,q,()~dbwrite.sort[42],1,,wrong type (long) returns () +true,0,0,q,1=logcount,1,,logerr called once for wrong type + +/ Test 9: sort - sorting and attribute application when config entry exists for table +run,0,0,q,`:dbwrite_sort_tp/.d set `sym`price,1,,write column order file +run,0,0,q,`:dbwrite_sort_tp/sym set `IBM`AAPL`MSFT,1,,write unsorted sym column +run,0,0,q,`:dbwrite_sort_tp/price set 200 100 300f,1,,write price column +run,0,0,q,"dbwrite.sort[(`trade;`:dbwrite_sort_tp/)]",1,,sort trade table in test partition +true,0,0,q,`AAPL`IBM`MSFT~exec sym from get `:dbwrite_sort_tp/,1,,sorted ascending by sym +true,0,0,q,`p=attr get `:dbwrite_sort_tp/sym,1,,p attribute applied to sym on disk + +/ Test 10: sort - default row used after resetting params with loadconfig null +run,0,0,q,dbwrite.loadconfig[`],1,,reset params to default row via null loadconfig +run,0,0,q,`:dbwrite_reset_tp/.d set `time`sym,1,,write column order file +run,0,0,q,`:dbwrite_reset_tp/time set 2024.01.01D09:00:00.000000000 2024.01.01D08:00:00.000000000,1,,write unsorted time column +run,0,0,q,`:dbwrite_reset_tp/sym set `IBM`AAPL,1,,write sym column +run,0,0,q,`:dbwrite_reset_tp/price set 200 100f,1,,write price column +run,0,0,q,logcount:0,1,,reset counter +run,0,0,q,"dbwrite.sort[(`othertable;`:dbwrite_reset_tp/)]",1,,sort using default row +true,0,0,q,(asc exec time from get `:dbwrite_reset_tp/)~exec time from get `:dbwrite_reset_tp/,1,,sorted ascending by time via default row +true,0,0,q,4=logcount,1,,loginfo x2 (sorting start + sort-by) + logwarn x1 (using defaults) + loginfo x1 (finished) + +/ Test 11: gc calls loginfo twice and does not throw +run,0,0,q,logcount:0,1,,reset counter +run,0,0,q,dbwrite.gc[],1,,run garbage collect +true,0,0,q,2=logcount,1,,loginfo called twice (start and end) + +/ Test 12: savedown - write table to hdb partition +run,0,0,q,dbwrite.loadconfig[`],1,,ensure default sort params (by time) +run,0,0,q,"sdtbl:([]time:2024.01.01D09:00:00.000000000 2024.01.01D08:00:00.000000000;sym:`IBM`AAPL;price:100 200f)",1,,unsorted test table +run,0,0,q,"dbwrite.savedown[`:dbwrite_sd_hdb;2024.01.01;`trade;sdtbl]",1,,write to hdb partition +run,0,0,q,"sdpath:` sv (.Q.par[`:dbwrite_sd_hdb;2024.01.01;`trade];`)",1,,partition path with trailing slash for xasc compatibility +true,0,0,q,2=count get sdpath,1,,two rows written to partition +true,0,0,q,(asc exec time from get sdpath)~exec time from get sdpath,1,,rows sorted by time ascending after sort +true,0,0,q,0