vectrojs 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/LICENSE +21 -0
- package/README.md +822 -2
- package/bin/vectro.mjs +82 -0
- package/dist/vectro.wasm +0 -0
- package/mcp/server.mjs +288 -0
- package/package.json +91 -4
- package/src/arrow.mjs +905 -0
- package/src/bitpack.mjs +135 -0
- package/src/browser.mjs +129 -0
- package/src/bytes.mjs +60 -0
- package/src/csv-kernel.mjs +205 -0
- package/src/csv.mjs +270 -0
- package/src/dataset.mjs +1763 -0
- package/src/dates.mjs +139 -0
- package/src/emitter.mjs +109 -0
- package/src/engine.mjs +881 -0
- package/src/files-browser.mjs +104 -0
- package/src/files-node.mjs +59 -0
- package/src/frames.mjs +191 -0
- package/src/fuse.mjs +512 -0
- package/src/interp.mjs +305 -0
- package/src/join.mjs +534 -0
- package/src/kernels.mjs +312 -0
- package/src/ndjson.mjs +136 -0
- package/src/node.mjs +120 -0
- package/src/pack.mjs +114 -0
- package/src/parallel-sort.mjs +99 -0
- package/src/partition-kernels.mjs +388 -0
- package/src/partition.mjs +395 -0
- package/src/plan.mjs +551 -0
- package/src/portable.mjs +21 -0
- package/src/rank-kernels.mjs +81 -0
- package/src/rows.mjs +91 -0
- package/src/serve/catalog.mjs +148 -0
- package/src/serve/flight.mjs +270 -0
- package/src/serve/formats.mjs +87 -0
- package/src/serve/http.mjs +182 -0
- package/src/serve/index.mjs +9 -0
- package/src/serve/odata.mjs +291 -0
- package/src/serve/pg.mjs +588 -0
- package/src/serve/pgcatalog.mjs +220 -0
- package/src/serve/pgtypes.mjs +199 -0
- package/src/sketch.mjs +75 -0
- package/src/sort-kernels.mjs +117 -0
- package/src/sort.mjs +102 -0
- package/src/split-worker-core.mjs +252 -0
- package/src/split-worker-node.mjs +8 -0
- package/src/split-worker.mjs +7 -0
- package/src/split.mjs +741 -0
- package/src/sql.mjs +1558 -0
- package/src/sqltext.mjs +132 -0
- package/src/static-kernels.mjs +164 -0
- package/src/stream-worker.mjs +553 -0
- package/src/table-kernels.mjs +323 -0
- package/src/table.mjs +2206 -0
- package/src/textmap.mjs +159 -0
- package/src/vectors.mjs +138 -0
- package/src/wasm.mjs +254 -0
- package/src/window.mjs +160 -0
- package/src/worker-browser.mjs +21 -0
- package/src/worker-loop.mjs +106 -0
- package/src/worker.mjs +22 -0
- package/src/zones.mjs +75 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// PostgreSQL's system catalog for the tables Vectro serves (pg.mjs): the
|
|
2
|
+
// pg_catalog and information_schema tables clients read to list tables and
|
|
3
|
+
// columns, built as Vectro tables so plain catalog SQL runs on them; exact
|
|
4
|
+
// answers for the long metadata queries drivers send (Npgsql's type loading
|
|
5
|
+
// for Power BI, pgJDBC's getTables / getColumns for Tableau and others),
|
|
6
|
+
// recognised by their text; and for any other catalog query, an empty
|
|
7
|
+
// answer with its column names (no keys, no indexes, no comments).
|
|
8
|
+
import { columnType } from '../sql.mjs';
|
|
9
|
+
import { TYPE_OF, OID } from './pgtypes.mjs';
|
|
10
|
+
|
|
11
|
+
export const NS = { pg_catalog: 11, public: 2200, information_schema: 13000 };
|
|
12
|
+
const OWNER = 10, FIRST_TABLE = 16384;
|
|
13
|
+
|
|
14
|
+
// [name, oid, length, category, array oid] of the types the catalog lists
|
|
15
|
+
const BASE_TYPES = [
|
|
16
|
+
['bool', 16, 1, 'B', 1000], ['bytea', 17, -1, 'U', 1001], ['char', 18, 1, 'Z', 1002], ['name', 19, 64, 'S', 1003],
|
|
17
|
+
['int8', 20, 8, 'N', 1016], ['int2', 21, 2, 'N', 1005], ['int4', 23, 4, 'N', 1007], ['regproc', 24, 4, 'N', 1008],
|
|
18
|
+
['text', 25, -1, 'S', 1009], ['oid', 26, 4, 'N', 1028], ['json', 114, -1, 'U', 199], ['xml', 142, -1, 'U', 143],
|
|
19
|
+
['float4', 700, 4, 'N', 1021], ['float8', 701, 8, 'N', 1022], ['money', 790, 8, 'N', 791], ['bpchar', 1042, -1, 'S', 1014],
|
|
20
|
+
['varchar', 1043, -1, 'S', 1015], ['date', 1082, 4, 'D', 1182], ['time', 1083, 8, 'D', 1183], ['timestamp', 1114, 8, 'D', 1115],
|
|
21
|
+
['timestamptz', 1184, 8, 'D', 1185], ['interval', 1186, 16, 'T', 1187], ['timetz', 1266, 12, 'D', 1270], ['bit', 1560, -1, 'V', 1561],
|
|
22
|
+
['varbit', 1562, -1, 'V', 1563], ['numeric', 1700, -1, 'N', 1231], ['regclass', 2205, 4, 'N', 2210], ['regtype', 2206, 4, 'N', 2211],
|
|
23
|
+
['uuid', 2950, 16, 'U', 2951], ['jsonb', 3802, -1, 'U', 3807],
|
|
24
|
+
];
|
|
25
|
+
const PSEUDO_TYPES = [['record', 2249, -1, 'P', 2287], ['void', 2278, 4, 'P', 0], ['unknown', 705, -2, 'X', 0]];
|
|
26
|
+
export const TYPES = [
|
|
27
|
+
...BASE_TYPES.map(([name, oid, len, cat, arr]) => ({ name, oid, len, cat, arr, typtype: 'b', elem: 0 })),
|
|
28
|
+
...BASE_TYPES.map(([name, oid, , , arr]) => ({ name: `_${name}`, oid: arr, len: -1, cat: 'A', arr: 0, typtype: 'b', elem: oid })),
|
|
29
|
+
...PSEUDO_TYPES.map(([name, oid, len, cat, arr]) => ({ name, oid, len, cat, arr, typtype: 'p', elem: 0 })),
|
|
30
|
+
];
|
|
31
|
+
// information_schema's names for our types
|
|
32
|
+
const SQL_NAMES = { int8: 'bigint', float8: 'double precision', bool: 'boolean', text: 'text', timestamp: 'timestamp without time zone', date: 'date' };
|
|
33
|
+
|
|
34
|
+
// The tables' description: [{ name, oid, rows, columns: [{ name, type (pg type) }] }]
|
|
35
|
+
export function describeTables(named) {
|
|
36
|
+
return Object.entries(named).filter(([n]) => !n.includes('.')).map(([name, t], i) => ({
|
|
37
|
+
name, oid: FIRST_TABLE + i, rows: t.length,
|
|
38
|
+
columns: t.columnNames.map((c) => ({ name: c, type: TYPE_OF[columnType(t, c)] ?? 'float8' })),
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A Vectro table from column kinds and rows: spec = { col: 'text' | 'num' }
|
|
43
|
+
function table(engine, spec, rows) {
|
|
44
|
+
const names = Object.keys(spec);
|
|
45
|
+
return engine.table(Object.fromEntries(names.map((n, k) => [n, spec[n] === 'text'
|
|
46
|
+
? (rows.length ? rows.map((r) => (r[k] === null || r[k] === undefined ? '' : String(r[k]))) : { codes: new Uint32Array(0), labels: [] })
|
|
47
|
+
: Float64Array.from(rows, (r) => (r[k] === null || r[k] === undefined ? NaN : typeof r[k] === 'boolean' ? +r[k] : r[k])),
|
|
48
|
+
])));
|
|
49
|
+
}
|
|
50
|
+
const cols = (s) => Object.fromEntries(s.split(/\s+/).filter(Boolean).map((x) => { const [n, k] = x.split(':'); return [n, k === 't' ? 'text' : 'num']; }));
|
|
51
|
+
|
|
52
|
+
// pg_catalog and information_schema as Vectro tables, keyed "schema.name".
|
|
53
|
+
export function catalogTables(engine, described, { database, user, settings }) {
|
|
54
|
+
const T = {};
|
|
55
|
+
const add = (key, spec, rows = []) => { T[key] = table(engine, cols(spec), rows); };
|
|
56
|
+
const typeOid = (t) => OID[t] ?? OID.text;
|
|
57
|
+
const schemas = [['pg_catalog', NS.pg_catalog], ['public', NS.public], ['information_schema', NS.information_schema]];
|
|
58
|
+
add('pg_catalog.pg_namespace', 'oid nspname:t nspowner nspacl:t', schemas.map(([n, o]) => [o, n, OWNER, '']));
|
|
59
|
+
add('pg_catalog.pg_class', 'oid relname:t relnamespace relkind:t relowner reltuples relpages relhasindex relpersistence:t relispartition reltype reloftype relam relnatts relchecks relhasrules relhastriggers relhassubclass relrowsecurity relforcerowsecurity relreplident:t relacl:t reloptions:t relpartbound:t reltablespace relfilenode',
|
|
60
|
+
described.map((d) => [d.oid, d.name, NS.public, 'r', OWNER, d.rows, 0, 0, 'p', 0, 0, 0, 2, d.columns.length, 0, 0, 0, 0, 0, 0, 'd', '', '', '', 0, d.oid]));
|
|
61
|
+
add('pg_catalog.pg_attribute', 'attrelid attname:t atttypid attnum attlen atttypmod attnotnull attisdropped atthasdef atthasmissing attidentity:t attgenerated:t attcollation attndims attstattarget attislocal attinhcount attbyval:t attalign:t attstorage:t',
|
|
62
|
+
described.flatMap((d) => d.columns.map((c, k) => [d.oid, c.name, typeOid(c.type), k + 1, TYPES.find((x) => x.name === c.type)?.len ?? -1, -1, 0, 0, 0, 0, '', '', c.type === 'text' ? 100 : 0, 0, -1, 1, 0, '', 'd', 'p'])));
|
|
63
|
+
add('pg_catalog.pg_type', 'oid typname:t typnamespace typowner typlen typbyval typtype:t typcategory:t typispreferred typisdefined typdelim:t typrelid typelem typarray typinput:t typoutput:t typreceive:t typsend:t typalign:t typstorage:t typnotnull typbasetype typtypmod typndims typcollation typdefault:t typacl:t',
|
|
64
|
+
TYPES.map((t) => [t.oid, t.name, NS.pg_catalog, OWNER, t.len, t.len > 0 && t.len <= 8 ? 1 : 0, t.typtype, t.cat, 0, 1, ',', 0, t.elem, t.arr, t.elem ? 'array_in' : `${t.name}in`, t.elem ? 'array_out' : `${t.name}out`, t.elem ? 'array_recv' : `${t.name}recv`, t.elem ? 'array_send' : `${t.name}send`, 'd', 'p', 0, 0, -1, 0, t.name === 'text' || t.name === 'varchar' ? 100 : 0, '', '']));
|
|
65
|
+
add('pg_catalog.pg_database', 'oid datname:t datdba encoding datlocprovider:t datistemplate datallowconn datconnlimit datcollate:t datctype:t daticulocale:t daticurules:t datcollversion:t datacl:t dattablespace datfrozenxid datminmxid', [[16000, database, OWNER, 6, 'c', 0, 1, -1, 'C', 'C', '', '', '', '', 1663, 0, 0]]);
|
|
66
|
+
add('pg_catalog.pg_settings', 'name:t setting:t unit:t category:t short_desc:t extra_desc:t context:t vartype:t source:t min_val:t max_val:t enumvals:t boot_val:t reset_val:t sourcefile:t sourceline pending_restart',
|
|
67
|
+
Object.entries(settings).map(([k, v]) => [k, v, '', 'Vectro', '', '', 'user', 'string', 'default', '', '', '', v, v, '', NaN, 0]));
|
|
68
|
+
add('pg_catalog.pg_roles', 'oid rolname:t rolsuper rolinherit rolcreaterole rolcreatedb rolcanlogin rolreplication rolconnlimit rolpassword:t rolvaliduntil rolbypassrls rolconfig:t', [[OWNER, user, 0, 1, 0, 0, 1, 0, -1, '********', NaN, 0, '']]);
|
|
69
|
+
add('pg_catalog.pg_user', 'usename:t usesysid usecreatedb usesuper userepl usebypassrls passwd:t valuntil useconfig:t', [[user, OWNER, 0, 0, 0, 0, '********', NaN, '']]);
|
|
70
|
+
add('pg_catalog.pg_authid', 'oid rolname:t rolsuper rolcanlogin', [[OWNER, user, 0, 1]]);
|
|
71
|
+
add('pg_catalog.pg_tables', 'schemaname:t tablename:t tableowner:t tablespace:t hasindexes hasrules hastriggers rowsecurity', described.map((d) => ['public', d.name, user, '', 0, 0, 0, 0]));
|
|
72
|
+
add('pg_catalog.pg_am', 'oid amname:t amhandler:t amtype:t', [[2, 'heap', 'heap_tableam_handler', 't'], [403, 'btree', 'bthandler', 'i']]);
|
|
73
|
+
add('pg_catalog.pg_tablespace', 'oid spcname:t spcowner spcacl:t spcoptions:t', [[1663, 'pg_default', OWNER, '', ''], [1664, 'pg_global', OWNER, '', '']]);
|
|
74
|
+
add('pg_catalog.pg_proc', 'oid proname:t pronamespace proowner prolang prokind:t prorettype proargtypes:t proretset pronargs');
|
|
75
|
+
add('pg_catalog.pg_views', 'schemaname:t viewname:t viewowner:t definition:t');
|
|
76
|
+
add('pg_catalog.pg_matviews', 'schemaname:t matviewname:t matviewowner:t tablespace:t hasindexes ispopulated definition:t');
|
|
77
|
+
add('pg_catalog.pg_description', 'objoid classoid objsubid description:t');
|
|
78
|
+
add('pg_catalog.pg_shdescription', 'objoid classoid description:t');
|
|
79
|
+
add('pg_catalog.pg_attrdef', 'oid adrelid adnum adbin:t');
|
|
80
|
+
add('pg_catalog.pg_index', 'indexrelid indrelid indnatts indnkeyatts indisunique indisprimary indisexclusion indimmediate indisclustered indisvalid indisready indislive indkey:t indexprs:t indpred:t');
|
|
81
|
+
add('pg_catalog.pg_constraint', 'oid conname:t connamespace contype:t condeferrable condeferred convalidated conrelid contypid conindid conparentid confrelid confupdtype:t confdeltype:t confmatchtype:t conislocal coninhcount connoinherit conkey:t confkey:t conbin:t');
|
|
82
|
+
add('pg_catalog.pg_inherits', 'inhrelid inhparent inhseqno');
|
|
83
|
+
add('pg_catalog.pg_enum', 'oid enumtypid enumsortorder enumlabel:t');
|
|
84
|
+
add('pg_catalog.pg_range', 'rngtypid rngsubtype rngmultitypid rngcollation');
|
|
85
|
+
add('pg_catalog.pg_extension', 'oid extname:t extowner extnamespace extrelocatable extversion:t');
|
|
86
|
+
add('pg_catalog.pg_collation', 'oid collname:t collnamespace collowner collprovider:t collencoding collcollate:t collctype:t');
|
|
87
|
+
add('pg_catalog.pg_trigger', 'oid tgrelid tgname:t tgfoid tgtype tgenabled:t tgisinternal');
|
|
88
|
+
add('pg_catalog.pg_rewrite', 'oid rulename:t ev_class ev_type:t ev_enabled:t is_instead');
|
|
89
|
+
add('pg_catalog.pg_sequence', 'seqrelid seqtypid seqstart seqincrement seqmax seqmin seqcache seqcycle');
|
|
90
|
+
add('pg_catalog.pg_foreign_table', 'ftrelid ftserver ftoptions:t');
|
|
91
|
+
add('pg_catalog.pg_partitioned_table', 'partrelid partstrat:t partnatts partdefid');
|
|
92
|
+
for (const k of ['pg_stat_user_tables', 'pg_stat_all_tables']) add(`pg_catalog.${k}`, 'relid schemaname:t relname:t seq_scan seq_tup_read idx_scan idx_tup_fetch n_tup_ins n_tup_upd n_tup_del n_live_tup n_dead_tup n_mod_since_analyze last_vacuum last_autovacuum last_analyze last_autoanalyze',
|
|
93
|
+
described.map((d) => [d.oid, 'public', d.name, 0, 0, 0, 0, 0, 0, 0, d.rows, 0, 0, NaN, NaN, NaN, NaN]));
|
|
94
|
+
add('pg_catalog.pg_stat_activity', 'datid datname:t pid usename:t application_name:t client_addr:t state:t query:t');
|
|
95
|
+
add('pg_catalog.pg_event_trigger', 'oid evtname:t evtevent:t evtenabled:t');
|
|
96
|
+
add('pg_catalog.pg_publication', 'oid pubname:t');
|
|
97
|
+
// information_schema
|
|
98
|
+
const db = database;
|
|
99
|
+
add('information_schema.schemata', 'catalog_name:t schema_name:t schema_owner:t default_character_set_catalog:t default_character_set_schema:t default_character_set_name:t sql_path:t', schemas.map(([n]) => [db, n, user, '', '', '', '']));
|
|
100
|
+
add('information_schema.tables', 'table_catalog:t table_schema:t table_name:t table_type:t self_referencing_column_name:t reference_generation:t user_defined_type_catalog:t user_defined_type_schema:t user_defined_type_name:t is_insertable_into:t is_typed:t commit_action:t',
|
|
101
|
+
described.map((d) => [db, 'public', d.name, 'BASE TABLE', '', '', '', '', '', 'NO', 'NO', '']));
|
|
102
|
+
add('information_schema.columns', 'table_catalog:t table_schema:t table_name:t column_name:t ordinal_position column_default:t is_nullable:t data_type:t character_maximum_length character_octet_length numeric_precision numeric_precision_radix numeric_scale datetime_precision interval_type:t interval_precision character_set_catalog:t character_set_schema:t character_set_name:t collation_catalog:t collation_schema:t collation_name:t domain_catalog:t domain_schema:t domain_name:t udt_catalog:t udt_schema:t udt_name:t scope_catalog:t scope_schema:t scope_name:t maximum_cardinality dtd_identifier:t is_self_referencing:t is_identity:t identity_generation:t identity_start:t identity_increment:t identity_maximum:t identity_minimum:t identity_cycle:t is_generated:t generation_expression:t is_updatable:t',
|
|
103
|
+
described.flatMap((d) => d.columns.map((c, k) => {
|
|
104
|
+
const num = c.type === 'int8' || c.type === 'float8';
|
|
105
|
+
return [db, 'public', d.name, c.name, k + 1, '', 'YES', SQL_NAMES[c.type] ?? c.type, NaN, c.type === 'text' ? 1073741824 : NaN, c.type === 'int8' ? 64 : c.type === 'float8' ? 53 : NaN, num ? 2 : NaN, c.type === 'int8' ? 0 : NaN, c.type === 'timestamp' ? 6 : c.type === 'date' ? 0 : NaN, '', NaN, '', '', '', '', '', '', '', '', '', db, 'pg_catalog', c.type, '', '', '', NaN, String(k + 1), 'NO', 'NO', '', '', '', '', '', 'NO', 'NEVER', '', 'NO'];
|
|
106
|
+
})));
|
|
107
|
+
add('information_schema.views', 'table_catalog:t table_schema:t table_name:t view_definition:t check_option:t is_updatable:t is_insertable_into:t');
|
|
108
|
+
add('information_schema.table_constraints', 'constraint_catalog:t constraint_schema:t constraint_name:t table_catalog:t table_schema:t table_name:t constraint_type:t is_deferrable:t initially_deferred:t enforced:t');
|
|
109
|
+
add('information_schema.key_column_usage', 'constraint_catalog:t constraint_schema:t constraint_name:t table_catalog:t table_schema:t table_name:t column_name:t ordinal_position position_in_unique_constraint');
|
|
110
|
+
add('information_schema.referential_constraints', 'constraint_catalog:t constraint_schema:t constraint_name:t unique_constraint_catalog:t unique_constraint_schema:t unique_constraint_name:t match_option:t update_rule:t delete_rule:t');
|
|
111
|
+
add('information_schema.constraint_column_usage', 'table_catalog:t table_schema:t table_name:t column_name:t constraint_catalog:t constraint_schema:t constraint_name:t');
|
|
112
|
+
add('information_schema.routines', 'specific_catalog:t specific_schema:t specific_name:t routine_catalog:t routine_schema:t routine_name:t routine_type:t data_type:t');
|
|
113
|
+
add('information_schema.parameters', 'specific_catalog:t specific_schema:t specific_name:t ordinal_position parameter_mode:t parameter_name:t data_type:t');
|
|
114
|
+
add('information_schema.table_privileges', 'grantor:t grantee:t table_catalog:t table_schema:t table_name:t privilege_type:t is_grantable:t with_hierarchy:t',
|
|
115
|
+
described.map((d) => [user, user, db, 'public', d.name, 'SELECT', 'NO', 'NO']));
|
|
116
|
+
add('information_schema.column_privileges', 'grantor:t grantee:t table_catalog:t table_schema:t table_name:t column_name:t privilege_type:t is_grantable:t');
|
|
117
|
+
add('information_schema.character_sets', 'character_set_catalog:t character_set_schema:t character_set_name:t character_repertoire:t form_of_use:t default_collate_catalog:t default_collate_schema:t default_collate_name:t', [['', '', 'UTF8', 'UCS', 'UTF8', db, 'pg_catalog', 'C']]);
|
|
118
|
+
add('information_schema.sequences', 'sequence_catalog:t sequence_schema:t sequence_name:t data_type:t');
|
|
119
|
+
add('information_schema.triggers', 'trigger_catalog:t trigger_schema:t trigger_name:t event_object_table:t');
|
|
120
|
+
return T;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---- driver queries answered as they are -----------------------------------------------
|
|
124
|
+
// SQL LIKE pattern (with \ escapes) -> RegExp
|
|
125
|
+
function likeRegex(p) {
|
|
126
|
+
let re = '';
|
|
127
|
+
for (let i = 0; i < p.length; i++) {
|
|
128
|
+
const c = p[i];
|
|
129
|
+
if (c === '\\' && i + 1 < p.length) { re += p[++i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); continue; }
|
|
130
|
+
re += c === '%' ? '[\\s\\S]*' : c === '_' ? '[\\s\\S]' : c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
131
|
+
}
|
|
132
|
+
return new RegExp(`^${re}$`);
|
|
133
|
+
}
|
|
134
|
+
// the pattern a query compares a name with (LIKE 'x' or = 'x'), or null for any
|
|
135
|
+
function patternFor(text, field) {
|
|
136
|
+
const f = field.replace('.', '\\.');
|
|
137
|
+
const where = text.slice(Math.max(0, text.search(/\bWHERE\b/i))); // (the filters, not a CASE before them)
|
|
138
|
+
const m = new RegExp(`${f}\\s+LIKE\\s+E?'((?:[^']|'')*)'`, 'i').exec(text) ?? new RegExp(`${f}\\s+=\\s+E?'((?:[^']|'')*)'`, 'i').exec(where);
|
|
139
|
+
if (!m) return null;
|
|
140
|
+
const v = m[1].replaceAll("''", "'");
|
|
141
|
+
return /LIKE/i.test(m[0]) ? likeRegex(v) : new RegExp(`^${v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
|
142
|
+
}
|
|
143
|
+
const ok = (re, s) => !re || re.test(s);
|
|
144
|
+
const result = (spec, rows) => ({ columns: spec.split(/\s+/).filter(Boolean).map((x) => { const [name, type] = x.split(':'); return { name, type }; }), rows });
|
|
145
|
+
|
|
146
|
+
// A known driver query -> { columns: [{ name, type }], rows }, or null.
|
|
147
|
+
export function driverAnswer(text, described, database = 'vectro') {
|
|
148
|
+
if (/\/\*\*\* Load all supported types \*\*\*\//.test(text)) { // Npgsql (Power BI): every type it may meet
|
|
149
|
+
const rows = [];
|
|
150
|
+
for (const t of TYPES) {
|
|
151
|
+
if (t.typtype === 'p') { if (t.name === 'record' || t.name === 'void') rows.push(['pg_catalog', t.name, t.oid, 0, 0, 'p', 0, 0]); continue; }
|
|
152
|
+
if (t.elem) rows.push(['pg_catalog', t.name, t.oid, 0, 0, 'a', t.elem, 3]);
|
|
153
|
+
else rows.push(['pg_catalog', t.name, t.oid, 0, 0, 'b', 0, 0]);
|
|
154
|
+
}
|
|
155
|
+
rows.sort((a, b) => a[7] - b[7]);
|
|
156
|
+
return result('nspname:name typname:name oid:oid typrelid:oid typbasetype:oid type:char elemoid:oid ord:int4', rows);
|
|
157
|
+
}
|
|
158
|
+
if (/\/\*\*\* Load field definitions for \(free-standing\) composite types \*\*\*\//.test(text)) return result('oid:oid attname:name atttypid:oid', []);
|
|
159
|
+
if (/\/\*\*\* Load enum fields \*\*\*\//.test(text)) return result('oid:oid enumlabel:name', []);
|
|
160
|
+
// pgJDBC (Tableau, DBeaver, Metabase): DatabaseMetaData
|
|
161
|
+
const schemas = /AS "?TABLE_SCHEM"?,\s*(NULL|current_database\(\)) AS "?TABLE_CATALOG"?\s+FROM pg_catalog\.pg_namespace/i.exec(text);
|
|
162
|
+
if (schemas) { // (older pgJDBC: NULL as the catalog; newer: current_database() and quoted upper-case names)
|
|
163
|
+
const re = patternFor(text, 'nspname'), quoted = /AS "TABLE_SCHEM"/.test(text);
|
|
164
|
+
return result(`${quoted ? 'TABLE_SCHEM' : 'table_schem'}:name ${quoted ? 'TABLE_CATALOG' : 'table_catalog'}:${quoted ? 'name' : 'text'}`, ['information_schema', 'pg_catalog', 'public'].filter((x) => ok(re, x)).map((x) => [x, /NULL/i.test(schemas[1]) ? null : database]));
|
|
165
|
+
}
|
|
166
|
+
if (/AS TABLE_TYPE,\s*d\.description AS REMARKS/i.test(text)) {
|
|
167
|
+
const sre = patternFor(text, 'n.nspname'), tre = patternFor(text, 'c.relname');
|
|
168
|
+
const wantTables = /c\.relkind = 'r'/.test(text) || !/c\.relkind/.test(text);
|
|
169
|
+
const rows = wantTables && ok(sre, 'public') ? described.filter((d) => ok(tre, d.name)).map((d) => [null, 'public', d.name, 'TABLE', null, '', '', '', '', '']) : [];
|
|
170
|
+
return result('table_cat:text table_schem:name table_name:name table_type:text remarks:text type_cat:text type_schem:text type_name:text self_referencing_col_name:text ref_generation:text', rows);
|
|
171
|
+
}
|
|
172
|
+
if (/row_number\(\) OVER \(PARTITION BY a\.attrelid ORDER BY a\.attnum\) AS attnum/i.test(text)) {
|
|
173
|
+
const sre = patternFor(text, 'n.nspname'), tre = patternFor(text, 'c.relname'), cre = patternFor(text, 'attname');
|
|
174
|
+
const rows = [];
|
|
175
|
+
if (ok(sre, 'public')) {
|
|
176
|
+
for (const d of described) {
|
|
177
|
+
if (!ok(tre, d.name)) continue;
|
|
178
|
+
d.columns.forEach((c, k) => { if (ok(cre, c.name)) rows.push(['public', d.name, c.name, OID[c.type], false, -1, TYPES.find((x) => x.name === c.type)?.len ?? -1, -1, k + 1, null, null, null, null, 0, 'b']); });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return result('nspname:name relname:name attname:name atttypid:oid attnotnull:bool atttypmod:int4 attlen:int2 typtypmod:int4 attnum:int8 attidentity:char attgenerated:char adsrc:text description:text typbasetype:oid typtype:char', rows);
|
|
182
|
+
}
|
|
183
|
+
if (/SELECT t\.typname,\s*t\.oid FROM pg_catalog\.pg_type t\s+JOIN pg_catalog\.pg_namespace n/i.test(text)) {
|
|
184
|
+
return result('typname:name oid:oid', TYPES.map((t) => [t.name, t.oid]));
|
|
185
|
+
}
|
|
186
|
+
if (/typinput\s*=\s*'pg_catalog\.array_in'::regproc as is_array/i.test(text)) {
|
|
187
|
+
const byName = /typname\s*=\s*'([^']*)'/i.exec(text)?.[1], byOid = /pg_type\.oid\s*=\s*(\d+)/i.exec(text)?.[1];
|
|
188
|
+
const rows = TYPES.filter((t) => (byName === undefined || t.name === byName) && (byOid === undefined || t.oid === Number(byOid))).map((t) => [!!t.elem, t.typtype, t.name, t.oid]);
|
|
189
|
+
return result('is_array:bool typtype:char typname:name oid:oid', rows);
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Does a query read the catalog (pg_catalog, information_schema, pg_* tables)?
|
|
195
|
+
export const readsCatalog = (text) => /\b(pg_catalog|information_schema)\s*\.|\bpg_[a-z_]+\b/i.test(text);
|
|
196
|
+
|
|
197
|
+
// The names of a SELECT's columns, read roughly from its text (for an empty answer).
|
|
198
|
+
export function selectNames(text) {
|
|
199
|
+
const t = text.replace(/--[^\n]*|\/\*[\s\S]*?\*\//g, ' ');
|
|
200
|
+
const start = /\bselect\b/i.exec(t);
|
|
201
|
+
if (!start) return [];
|
|
202
|
+
let depth = 0, i = start.index + 6, item = '', end = t.length;
|
|
203
|
+
const items = [];
|
|
204
|
+
for (; i < t.length; i++) {
|
|
205
|
+
const c = t[i];
|
|
206
|
+
if (c === "'") { const j = t.indexOf("'", i + 1); item += t.slice(i, j < 0 ? t.length : j + 1); i = j < 0 ? t.length : j; continue; }
|
|
207
|
+
if (c === '"') { const j = t.indexOf('"', i + 1); item += t.slice(i, j < 0 ? t.length : j + 1); i = j < 0 ? t.length : j; continue; }
|
|
208
|
+
if (c === '(') depth++;
|
|
209
|
+
if (c === ')') depth--;
|
|
210
|
+
if (depth === 0 && /\bfrom\b/i.test(t.slice(i, i + 5)) && /\s/.test(t[i - 1] ?? ' ') && /\s/.test(t[i + 4] ?? ' ')) { end = i; break; }
|
|
211
|
+
if (depth === 0 && c === ',') { items.push(item); item = ''; continue; }
|
|
212
|
+
item += c;
|
|
213
|
+
}
|
|
214
|
+
if (end <= t.length) items.push(item);
|
|
215
|
+
return items.map((s) => s.trim()).filter(Boolean).map((s) => {
|
|
216
|
+
const as = /\bas\s+("?)([^"\s]+)\1\s*$/i.exec(s) ?? /("?)([A-Za-z_][\w$]*)\1\s*$/.exec(s);
|
|
217
|
+
if (!as) return '?column?';
|
|
218
|
+
return as[1] ? as[2] : as[2].toLowerCase();
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// PostgreSQL types for the wire protocol (pg.mjs): which type each answer
|
|
2
|
+
// column is said to have, and its values as text or binary, as PostgreSQL
|
|
3
|
+
// writes them; parameters sent by clients, read back into SQL literals.
|
|
4
|
+
//
|
|
5
|
+
// Vectro's column types (sql's opts.types) map to:
|
|
6
|
+
// int -> int8, float -> float8, bool -> bool, text -> text,
|
|
7
|
+
// date -> timestamp, day -> date
|
|
8
|
+
// Catalog answers (pgcatalog.mjs) use the others below too.
|
|
9
|
+
|
|
10
|
+
export const OID = {
|
|
11
|
+
bool: 16, bytea: 17, char: 18, name: 19, int8: 20, int2: 21, int4: 23, regproc: 24, text: 25, oid: 26,
|
|
12
|
+
float4: 700, float8: 701, unknown: 705, bpchar: 1042, varchar: 1043, date: 1082, time: 1083,
|
|
13
|
+
timestamp: 1114, timestamptz: 1184, interval: 1186, numeric: 1700, regclass: 2205, regtype: 2206,
|
|
14
|
+
};
|
|
15
|
+
export const TYPE_OF = { int: 'int8', float: 'float8', bool: 'bool', text: 'text', date: 'timestamp', day: 'date' };
|
|
16
|
+
// (size for RowDescription; -1: variable)
|
|
17
|
+
const LEN = { bool: 1, char: 1, int2: 2, int4: 4, oid: 4, regproc: 4, regclass: 4, regtype: 4, float4: 4, date: 4, int8: 8, float8: 8, timestamp: 8, timestamptz: 8, name: 64 };
|
|
18
|
+
export const typeLength = (t) => LEN[t] ?? -1;
|
|
19
|
+
|
|
20
|
+
const EPOCH_2000 = 946_684_800_000; // ms from 1970 to 2000-01-01, PostgreSQL's zero for dates and times
|
|
21
|
+
const p2 = (x) => (x < 10 ? `0${x}` : `${x}`);
|
|
22
|
+
|
|
23
|
+
function dayText(ms) {
|
|
24
|
+
const d = new Date(ms), y = d.getUTCFullYear();
|
|
25
|
+
const ys = y > 0 ? (y < 1000 ? String(y).padStart(4, '0') : String(y)) : String(1 - y).padStart(4, '0');
|
|
26
|
+
return `${ys}-${p2(d.getUTCMonth() + 1)}-${p2(d.getUTCDate())}${y > 0 ? '' : ' BC'}`;
|
|
27
|
+
}
|
|
28
|
+
function timestampText(ms) {
|
|
29
|
+
const d = new Date(ms), y = d.getUTCFullYear();
|
|
30
|
+
const day = dayText(ms).replace(' BC', '');
|
|
31
|
+
const f = ((ms % 1000) + 1000) % 1000;
|
|
32
|
+
const frac = f ? `.${String(Math.round(f * 1000)).padStart(6, '0').replace(/0+$/, '')}` : '';
|
|
33
|
+
return `${day} ${p2(d.getUTCHours())}:${p2(d.getUTCMinutes())}:${p2(d.getUTCSeconds())}${frac}${y > 0 ? '' : ' BC'}`;
|
|
34
|
+
}
|
|
35
|
+
// float8 as PostgreSQL writes it (shortest exact form; exponents with two digits)
|
|
36
|
+
function floatText(v) {
|
|
37
|
+
if (v === Infinity) return 'Infinity';
|
|
38
|
+
if (v === -Infinity) return '-Infinity';
|
|
39
|
+
const s = String(v);
|
|
40
|
+
return s.includes('e') ? s.replace(/e([+-])(\d)$/, 'e$10$2') : s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// value -> text (null for SQL NULL); v is a number for number types and
|
|
44
|
+
// dates (ms), a string for text types
|
|
45
|
+
// (keepEmpty: '' is an empty text, not SQL NULL as in Vectro's tables)
|
|
46
|
+
export function toPgText(type, v, keepEmpty = false) {
|
|
47
|
+
if (v === null || v === undefined) return null;
|
|
48
|
+
switch (type) {
|
|
49
|
+
case 'text': case 'varchar': case 'name': case 'bpchar': case 'char': case 'regclass': case 'regtype': case 'regproc':
|
|
50
|
+
if (typeof v === 'number') return v === v ? floatText(v) : null;
|
|
51
|
+
return v === '' && type === 'text' && !keepEmpty ? null : String(v);
|
|
52
|
+
case 'bool': return v !== v ? null : typeof v === 'string' ? (v === 't' || v === 'true' ? 't' : 'f') : v ? 't' : 'f';
|
|
53
|
+
case 'int8': case 'int4': case 'int2': case 'oid':
|
|
54
|
+
if (typeof v === 'string') return v;
|
|
55
|
+
return v === v && Number.isFinite(v) ? String(Math.round(v) || 0) : null;
|
|
56
|
+
case 'float8': case 'float4': case 'numeric': return v === v ? floatText(v) : null;
|
|
57
|
+
case 'date': return Number.isFinite(v) ? dayText(v) : null;
|
|
58
|
+
case 'timestamp': case 'timestamptz': return Number.isFinite(v) ? timestampText(v) + (type === 'timestamptz' ? '+00' : '') : null;
|
|
59
|
+
default: return typeof v === 'number' ? (v === v ? floatText(v) : null) : String(v);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// value -> binary bytes (null for SQL NULL)
|
|
64
|
+
export function toPgBinary(type, v, keepEmpty = false) {
|
|
65
|
+
if (v === null || v === undefined || (typeof v === 'number' && v !== v)) return null;
|
|
66
|
+
switch (type) {
|
|
67
|
+
case 'int8': { const b = Buffer.alloc(8); b.writeBigInt64BE(BigInt(Math.round(v))); return b; }
|
|
68
|
+
case 'int4': case 'oid': case 'regproc': case 'regclass': case 'regtype': { const b = Buffer.alloc(4); if (type === 'int4') b.writeInt32BE(Math.round(v)); else b.writeUInt32BE(Math.round(v) >>> 0); return b; }
|
|
69
|
+
case 'int2': { const b = Buffer.alloc(2); b.writeInt16BE(Math.round(v)); return b; }
|
|
70
|
+
case 'float8': { const b = Buffer.alloc(8); b.writeDoubleBE(v); return b; }
|
|
71
|
+
case 'float4': { const b = Buffer.alloc(4); b.writeFloatBE(v); return b; }
|
|
72
|
+
case 'bool': return Buffer.from([typeof v === 'string' ? (v === 't' || v === 'true' ? 1 : 0) : v ? 1 : 0]);
|
|
73
|
+
case 'char': return Buffer.from(String(v).slice(0, 1), 'utf8');
|
|
74
|
+
case 'date': { if (!Number.isFinite(v)) return null; const b = Buffer.alloc(4); b.writeInt32BE(Math.floor((v - EPOCH_2000) / 86_400_000)); return b; }
|
|
75
|
+
case 'timestamp': case 'timestamptz': { if (!Number.isFinite(v)) return null; const b = Buffer.alloc(8); b.writeBigInt64BE(BigInt(Math.round((v - EPOCH_2000) * 1000))); return b; }
|
|
76
|
+
default: { const t = toPgText(type, v, keepEmpty); return t === null ? null : Buffer.from(t, 'utf8'); }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Which of our encodings can go binary (a client asking binary for others gets text).
|
|
81
|
+
export const BINARY = new Set(['int8', 'int4', 'int2', 'oid', 'float8', 'float4', 'bool', 'date', 'timestamp', 'timestamptz', 'text', 'varchar', 'name', 'bpchar', 'char', 'regproc', 'regclass', 'regtype']);
|
|
82
|
+
|
|
83
|
+
const typeByOid = new Map(Object.entries(OID).map(([k, v]) => [v, k]));
|
|
84
|
+
export const typeName = (oid) => typeByOid.get(oid) ?? null;
|
|
85
|
+
|
|
86
|
+
// A bound parameter (bytes, format 0 text / 1 binary, its type oid or 0) as a SQL literal.
|
|
87
|
+
export function paramLiteral(bytes, format, oid) {
|
|
88
|
+
if (bytes === null) return 'NULL';
|
|
89
|
+
const type = typeByOid.get(oid);
|
|
90
|
+
const quote = (s) => `'${String(s).replaceAll("'", "''")}'`;
|
|
91
|
+
if (format === 1) {
|
|
92
|
+
switch (type) {
|
|
93
|
+
case 'int8': return bytes.readBigInt64BE().toString();
|
|
94
|
+
case 'int4': return String(bytes.readInt32BE());
|
|
95
|
+
case 'int2': return String(bytes.readInt16BE());
|
|
96
|
+
case 'oid': return String(bytes.readUInt32BE());
|
|
97
|
+
case 'float8': { const v = bytes.readDoubleBE(); return v === v ? `(${v})` : 'NULL'; }
|
|
98
|
+
case 'float4': { const v = bytes.readFloatBE(); return v === v ? `(${v})` : 'NULL'; }
|
|
99
|
+
case 'bool': return bytes[0] ? 'TRUE' : 'FALSE';
|
|
100
|
+
case 'date': return `DATE ${quote(dayText(EPOCH_2000 + bytes.readInt32BE() * 86_400_000))}`;
|
|
101
|
+
case 'timestamp': case 'timestamptz': return `TIMESTAMP ${quote(timestampText(EPOCH_2000 + Number(bytes.readBigInt64BE()) / 1000))}`;
|
|
102
|
+
case 'numeric': return numericLiteral(bytes);
|
|
103
|
+
default: return quote(bytes.toString('utf8'));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const s = bytes.toString('utf8');
|
|
107
|
+
switch (type) {
|
|
108
|
+
case 'int8': case 'int4': case 'int2': case 'oid': case 'float8': case 'float4': case 'numeric':
|
|
109
|
+
return /^\s*[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?\s*$/i.test(s) ? `(${s.trim()})` : /^\s*nan\s*$/i.test(s) ? 'NULL' : quote(s);
|
|
110
|
+
case 'bool': return /^\s*(t|true|y|yes|on|1)\s*$/i.test(s) ? 'TRUE' : 'FALSE';
|
|
111
|
+
case 'date': return `DATE ${quote(s)}`;
|
|
112
|
+
case 'timestamp': case 'timestamptz': return `TIMESTAMP ${quote(s)}`;
|
|
113
|
+
default: return quote(s);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// numeric in binary: ndigits, weight, sign, dscale, then base-10000 digits
|
|
117
|
+
function numericLiteral(b) {
|
|
118
|
+
const n = b.readInt16BE(0), weight = b.readInt16BE(2), sign = b.readUInt16BE(4);
|
|
119
|
+
if (sign === 0xc000) return 'NULL';
|
|
120
|
+
let v = 0;
|
|
121
|
+
for (let i = 0; i < n; i++) v += b.readInt16BE(8 + 2 * i) * 10_000 ** (weight - i);
|
|
122
|
+
return `(${sign === 0x4000 ? -v : v})`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Put parameters into a query: $1, $2... outside quotes, comments and names.
|
|
126
|
+
export function bindParams(text, literals) {
|
|
127
|
+
if (!literals.length) return text;
|
|
128
|
+
let out = '', i = 0;
|
|
129
|
+
while (i < text.length) {
|
|
130
|
+
const c = text[i];
|
|
131
|
+
if (c === "'" || c === '"') { // a quoted text or name, as it is
|
|
132
|
+
let j = i + 1;
|
|
133
|
+
for (; j < text.length; j++) if (text[j] === c) { if (text[j + 1] === c) j++; else break; }
|
|
134
|
+
out += text.slice(i, j + 1); i = j + 1; continue;
|
|
135
|
+
}
|
|
136
|
+
if (c === '-' && text[i + 1] === '-') { const j = text.indexOf('\n', i); out += text.slice(i, j < 0 ? text.length : j); i = j < 0 ? text.length : j; continue; }
|
|
137
|
+
if (c === '/' && text[i + 1] === '*') { const j = text.indexOf('*/', i + 2); const e = j < 0 ? text.length : j + 2; out += text.slice(i, e); i = e; continue; }
|
|
138
|
+
if (c === '$' && /\d/.test(text[i + 1] ?? '') && !/[\w$]/.test(text[i - 1] ?? '')) {
|
|
139
|
+
let j = i + 1;
|
|
140
|
+
while (/\d/.test(text[j] ?? '')) j++;
|
|
141
|
+
const k = Number(text.slice(i + 1, j));
|
|
142
|
+
if (k < 1 || k > literals.length) throw new PgError('08P01', `there is no parameter $${k}`);
|
|
143
|
+
out += literals[k - 1]; i = j; continue;
|
|
144
|
+
}
|
|
145
|
+
out += c; i++;
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// An error for the client, with its SQLSTATE code.
|
|
151
|
+
export class PgError extends Error { constructor(code, message) { super(message); this.code = code; } }
|
|
152
|
+
|
|
153
|
+
// ---- writing many rows: one growing buffer per batch, no Buffer per value -------------
|
|
154
|
+
const dayCache = new Map();
|
|
155
|
+
function fastTimestamp(ms) { // timestampText, with each day's text made once
|
|
156
|
+
if (!(ms > -6e13 && ms < 2.5e14)) return timestampText(ms); // (years before 100 or after 9999)
|
|
157
|
+
const day = Math.floor(ms / 86_400_000);
|
|
158
|
+
let d = dayCache.get(day);
|
|
159
|
+
if (d === undefined) { if (dayCache.size > 100_000) dayCache.clear(); d = dayText(day * 86_400_000); dayCache.set(day, d); }
|
|
160
|
+
let t = ms - day * 86_400_000;
|
|
161
|
+
const h = Math.floor(t / 3_600_000); t -= h * 3_600_000;
|
|
162
|
+
const m = Math.floor(t / 60_000); t -= m * 60_000;
|
|
163
|
+
const sec = Math.floor(t / 1000), f = t - sec * 1000;
|
|
164
|
+
const frac = f ? `.${String(Math.round(f * 1000)).padStart(6, '0').replace(/0+$/, '')}` : '';
|
|
165
|
+
return `${d} ${p2(h)}:${p2(m)}:${p2(sec)}${frac}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// DataRow messages for rows [from, to): cell(i, k) values, column types and formats
|
|
169
|
+
export function dataRows(columns, formats, cell, from, to, keepEmpty) {
|
|
170
|
+
const K = columns.length;
|
|
171
|
+
let buf = Buffer.allocUnsafe(Math.max(1024, (to - from) * (7 + K * 12))), at = 0;
|
|
172
|
+
const need = (n) => { if (at + n > buf.length) { const b = Buffer.allocUnsafe(Math.max(buf.length * 2, at + n)); buf.copy(b, 0, 0, at); buf = b; } };
|
|
173
|
+
const kinds = columns.map((c, k) => (formats[k] === 1 ? `b:${c.type}` : c.type === 'timestamp' ? 't:ts' : 't'));
|
|
174
|
+
for (let i = from; i < to; i++) {
|
|
175
|
+
need(7);
|
|
176
|
+
const start = at;
|
|
177
|
+
buf[at] = 0x44; at += 5; // 'D', length later
|
|
178
|
+
buf.writeInt16BE(K, at); at += 2;
|
|
179
|
+
for (let k = 0; k < K; k++) {
|
|
180
|
+
const v = cell(i, k), kind = kinds[k];
|
|
181
|
+
if (v === null || v === undefined || (typeof v === 'number' && v !== v)) { need(4); buf.writeInt32BE(-1, at); at += 4; continue; }
|
|
182
|
+
if (kind === 'b:int8' || kind === 'b:timestamp' || kind === 'b:timestamptz') {
|
|
183
|
+
const x = kind === 'b:int8' ? Math.round(v) : Math.round((v - EPOCH_2000) * 1000);
|
|
184
|
+
need(12); buf.writeInt32BE(8, at); const hi = Math.floor(x / 4294967296); buf.writeInt32BE(hi, at + 4); buf.writeUInt32BE(x - hi * 4294967296, at + 8); at += 12; continue;
|
|
185
|
+
}
|
|
186
|
+
if (kind === 'b:float8') { need(12); buf.writeInt32BE(8, at); buf.writeDoubleBE(v, at + 4); at += 12; continue; }
|
|
187
|
+
if (kind === 'b:date') { need(8); buf.writeInt32BE(4, at); buf.writeInt32BE(Math.floor((v - EPOCH_2000) / 86_400_000), at + 4); at += 8; continue; }
|
|
188
|
+
let bytes = null, text = null;
|
|
189
|
+
if (kind[0] === 'b') bytes = toPgBinary(columns[k].type, v, keepEmpty);
|
|
190
|
+
else text = kind === 't:ts' && typeof v === 'number' ? (Number.isFinite(v) ? fastTimestamp(v) : null) : toPgText(columns[k].type, v, keepEmpty);
|
|
191
|
+
if (bytes === null && text === null) { need(4); buf.writeInt32BE(-1, at); at += 4; continue; }
|
|
192
|
+
if (bytes) { need(4 + bytes.length); buf.writeInt32BE(bytes.length, at); bytes.copy(buf, at + 4); at += 4 + bytes.length; continue; }
|
|
193
|
+
const n = Buffer.byteLength(text);
|
|
194
|
+
need(4 + n); buf.writeInt32BE(n, at); buf.write(text, at + 4, 'utf8'); at += 4 + n;
|
|
195
|
+
}
|
|
196
|
+
buf.writeInt32BE(at - start - 1, start + 1);
|
|
197
|
+
}
|
|
198
|
+
return buf.subarray(0, at);
|
|
199
|
+
}
|
package/src/sketch.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Distinct-count sketches (HyperLogLog). A sketch is 4,096 one-byte registers
|
|
2
|
+
// (4 KB); adding a value sets one register to the largest "first 1 bit"
|
|
3
|
+
// position seen in its hash. Sketches merge by taking each register's max, so
|
|
4
|
+
// partial sketches from workers, time slices or other machines combine into
|
|
5
|
+
// the sketch of everything they saw. The estimate is within about 1.6% (one
|
|
6
|
+
// standard error) of the true number of distinct values.
|
|
7
|
+
//
|
|
8
|
+
// Values hash by what they are, not how they are stored: numbers by their
|
|
9
|
+
// bits (-0 as 0; NaN is skipped), text by its characters, so the same label
|
|
10
|
+
// hashes alike in every worker's dictionary.
|
|
11
|
+
|
|
12
|
+
export const SKETCH_BITS = 12;
|
|
13
|
+
export const SKETCH_SIZE = 1 << SKETCH_BITS; // registers (bytes) per sketch
|
|
14
|
+
|
|
15
|
+
const f64 = new Float64Array(1), u32 = new Uint32Array(f64.buffer);
|
|
16
|
+
const fmix = (h) => { // murmur3 finalizer
|
|
17
|
+
h ^= h >>> 16; h = Math.imul(h, 0x85ebca6b);
|
|
18
|
+
h ^= h >>> 13; h = Math.imul(h, 0xc2b2ae35);
|
|
19
|
+
return (h ^ (h >>> 16)) >>> 0;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// 32-bit hash of a number (NaN must be skipped by the caller).
|
|
23
|
+
export function hashNumber(v) {
|
|
24
|
+
f64[0] = v === 0 ? 0 : v;
|
|
25
|
+
return fmix(u32[0] ^ fmix(u32[1] ^ 0x9e3779b9));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 32-bit hash of a string.
|
|
29
|
+
export function hashText(s) {
|
|
30
|
+
let h = 0x811c9dc5;
|
|
31
|
+
for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 0x01000193);
|
|
32
|
+
return fmix(h ^ 0x5bd1e995);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Add a hash to the sketch at regs[at .. at + SKETCH_SIZE).
|
|
36
|
+
export function sketchAdd(regs, at, h) {
|
|
37
|
+
const j = h >>> (32 - SKETCH_BITS);
|
|
38
|
+
const rest = (h << SKETCH_BITS) >>> 0;
|
|
39
|
+
const rank = rest === 0 ? 32 - SKETCH_BITS + 1 : Math.clz32(rest) + 1;
|
|
40
|
+
if (rank > regs[at + j]) regs[at + j] = rank;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function sketchMerge(dst, dAt, src, sAt) {
|
|
44
|
+
for (let j = 0; j < SKETCH_SIZE; j++) if (src[sAt + j] > dst[dAt + j]) dst[dAt + j] = src[sAt + j];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ALPHA = 0.7213 / (1 + 1.079 / SKETCH_SIZE);
|
|
48
|
+
export function sketchEstimate(regs, at = 0) {
|
|
49
|
+
const m = SKETCH_SIZE;
|
|
50
|
+
let sum = 0, zeros = 0;
|
|
51
|
+
for (let j = 0; j < m; j++) { const r = regs[at + j]; sum += 2 ** -r; if (r === 0) zeros++; }
|
|
52
|
+
let e = (ALPHA * m * m) / sum;
|
|
53
|
+
if (e <= 2.5 * m && zeros) e = m * Math.log(m / zeros); // few values: linear counting
|
|
54
|
+
else if (e > 2 ** 32 / 30) e = -(2 ** 32) * Math.log(1 - e / 2 ** 32); // 32-bit hash: large-range correction
|
|
55
|
+
return Math.round(e);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A sketch as an object: add values, merge, estimate, ship as bytes.
|
|
59
|
+
// const s = new DistinctSketch(); s.add('EU'); s.add(42); s.estimate();
|
|
60
|
+
// DistinctSketch.fromBytes(bytes).merge(s)
|
|
61
|
+
export class DistinctSketch {
|
|
62
|
+
constructor(registers = new Uint8Array(SKETCH_SIZE)) {
|
|
63
|
+
if (registers.length !== SKETCH_SIZE) throw new RangeError(`vectro: a sketch has ${SKETCH_SIZE} registers, got ${registers.length}`);
|
|
64
|
+
this.registers = registers;
|
|
65
|
+
}
|
|
66
|
+
add(value) {
|
|
67
|
+
if (typeof value === 'number') { if (value === value) sketchAdd(this.registers, 0, hashNumber(value)); }
|
|
68
|
+
else if (value != null) sketchAdd(this.registers, 0, hashText(String(value)));
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
merge(other) { sketchMerge(this.registers, 0, other.registers, 0); return this; }
|
|
72
|
+
estimate() { return sketchEstimate(this.registers, 0); }
|
|
73
|
+
toBytes() { return this.registers.slice(); }
|
|
74
|
+
static fromBytes(bytes) { return new DistinctSketch(Uint8Array.from(bytes)); }
|
|
75
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// WebAssembly kernels for the parallel radix sort (see parallel-sort.mjs).
|
|
2
|
+
// Each runs over one unit of rows [start, end) and gets its unit number, so
|
|
3
|
+
// it can keep its own digit counts and write to its own output positions
|
|
4
|
+
// (OP.UNITS). Pointers are byte offsets into the engine's memory.
|
|
5
|
+
//
|
|
6
|
+
// Keys become two uint32 words (lo, hi) that sort as unsigned numbers:
|
|
7
|
+
// whole-number keys as their offset from the minimum, others as float bits
|
|
8
|
+
// with the sign flipped (negatives: every bit flipped; NaN last). Digits are
|
|
9
|
+
// 11 bits: lo 0-10, 11-21, 22-31, then hi the same.
|
|
10
|
+
|
|
11
|
+
import { I32, F64, Fn, buildModule, get, set, i32c, block, loop, end, br, brIf, i32add, i32mul, i32and, i32geu } from './wasm.mjs';
|
|
12
|
+
|
|
13
|
+
const I64 = 0x7e;
|
|
14
|
+
const I32_LOAD = [0x28, 2, 0], I32_STORE = [0x36, 2, 0];
|
|
15
|
+
const F64_LOAD = [0x2b, 3, 0], F32_LOAD = [0x2a, 2, 0], F64_STORE = [0x39, 3, 0], F32_STORE = [0x38, 2, 0];
|
|
16
|
+
const I32_SHR_U = [0x76], I32_SHL = [0x74], SELECT = [0x1b], IF = [0x04, 0x40], IF_I32 = [0x04, 0x7f], ELSE = [0x05];
|
|
17
|
+
const f64c = (v) => [0x44, ...new Uint8Array(Float64Array.of(v).buffer)];
|
|
18
|
+
const i64c = (v) => { // signed LEB128 of a BigInt
|
|
19
|
+
const out = [0x42];
|
|
20
|
+
for (;;) {
|
|
21
|
+
const b = Number(v & 0x7fn);
|
|
22
|
+
v >>= 7n;
|
|
23
|
+
if ((v === 0n && !(b & 0x40)) || (v === -1n && b & 0x40)) { out.push(b); return out; }
|
|
24
|
+
out.push(b | 0x80);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const addr4 = (base, i) => [...get(i), ...i32c(2), ...I32_SHL, ...get(base), ...i32add]; // base + i * 4
|
|
28
|
+
export const DIGIT_BITS = 11, BUCKETS = 2048;
|
|
29
|
+
export const DIGITS = [[0, 0], [0, 11], [0, 22], [1, 0], [1, 11], [1, 22]]; // [word (0 = lo), shift]
|
|
30
|
+
|
|
31
|
+
// prep(start, end, unit, keyPtr, idxPtr, hasIdx, loPtr, hiPtr, whole, histPtr, min)
|
|
32
|
+
// lo[i], hi[i] = words of key[idx[i]] (key[i] without idx); counts all six
|
|
33
|
+
// digits into hist[unit][digit][bucket].
|
|
34
|
+
export function prepModule(t, shared) {
|
|
35
|
+
const f = new Fn('k', [I32, I32, I32, I32, I32, I32, I32, I32, I32, I32, F64], []);
|
|
36
|
+
const [start, stop, unit, keyPtr, idxPtr, hasIdx, loPtr, hiPtr, whole, histPtr, min] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
37
|
+
const i = f.local(I32), r = f.local(I32), h = f.local(I32), l = f.local(I32), hb = f.local(I32), a = f.local(I32);
|
|
38
|
+
const v = f.local(F64), o = f.local(F64), hf = f.local(F64), b = f.local(I64);
|
|
39
|
+
f.emit(get(histPtr), get(unit), i32c(6 * BUCKETS * 4), i32mul, i32add, set(hb));
|
|
40
|
+
f.emit(get(start), set(i));
|
|
41
|
+
f.emit(block, loop, get(i), get(stop), i32geu, brIf(1));
|
|
42
|
+
f.emit(get(hasIdx), IF_I32, addr4(idxPtr, i), I32_LOAD, ELSE, get(i), end, set(r));
|
|
43
|
+
f.emit(get(r), i32c(t === 'f32' ? 2 : 3), I32_SHL, get(keyPtr), i32add, t === 'f32' ? [...F32_LOAD, 0xbb] : F64_LOAD, set(v));
|
|
44
|
+
f.emit(get(whole), IF);
|
|
45
|
+
f.emit(get(v), get(min), [0xa1], set(o)); // o = v - min
|
|
46
|
+
f.emit(get(o), f64c(4294967296), [0xa3], [0x9c], set(hf)); // hf = floor(o / 2^32)
|
|
47
|
+
f.emit(get(hf), [0xfc, 0x03], set(h), get(o), get(hf), f64c(4294967296), [0xa2], [0xa1], [0xfc, 0x03], set(l));
|
|
48
|
+
f.emit(ELSE);
|
|
49
|
+
f.emit(get(v), [0xbd], set(b)); // b = bits(v)
|
|
50
|
+
f.emit(get(b), i64c(-1n), [0x85], get(b), i64c(-0x8000000000000000n), [0x84], get(b), i64c(0n), [0x53], SELECT, set(b)); // b < 0 ? ~b : b | sign
|
|
51
|
+
f.emit(get(v), get(v), [0x62], IF, i64c(-1n), set(b), end); // NaN: all ones (last)
|
|
52
|
+
f.emit(get(b), i64c(32n), [0x88], [0xa7], set(h), get(b), [0xa7], set(l));
|
|
53
|
+
f.emit(end);
|
|
54
|
+
f.emit(addr4(loPtr, i), get(l), I32_STORE, addr4(hiPtr, i), get(h), I32_STORE);
|
|
55
|
+
DIGITS.forEach(([word, shift], d) => {
|
|
56
|
+
f.emit(get(word ? h : l), i32c(shift), I32_SHR_U, i32c(BUCKETS - 1), i32and, i32c(d * BUCKETS), i32add);
|
|
57
|
+
f.emit(i32c(2), I32_SHL, get(hb), i32add, set(a), get(a), get(a), I32_LOAD, i32c(1), i32add, I32_STORE);
|
|
58
|
+
});
|
|
59
|
+
f.emit(get(i), i32c(1), i32add, set(i), br(0), end, end);
|
|
60
|
+
return buildModule([f], shared);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// count(start, end, unit, wordPtr, shift, histPtr): hist[unit][bucket] of one digit.
|
|
64
|
+
export function countModule(shared) {
|
|
65
|
+
const f = new Fn('k', [I32, I32, I32, I32, I32, I32], []);
|
|
66
|
+
const [start, stop, unit, wordPtr, shift, histPtr] = [0, 1, 2, 3, 4, 5];
|
|
67
|
+
const i = f.local(I32), hb = f.local(I32), a = f.local(I32);
|
|
68
|
+
f.emit(get(histPtr), get(unit), i32c(BUCKETS * 4), i32mul, i32add, set(hb), get(start), set(i));
|
|
69
|
+
f.emit(block, loop, get(i), get(stop), i32geu, brIf(1));
|
|
70
|
+
f.emit(addr4(wordPtr, i), I32_LOAD, get(shift), I32_SHR_U, i32c(BUCKETS - 1), i32and, i32c(2), I32_SHL, get(hb), i32add, set(a));
|
|
71
|
+
f.emit(get(a), get(a), I32_LOAD, i32c(1), i32add, I32_STORE);
|
|
72
|
+
f.emit(get(i), i32c(1), i32add, set(i), br(0), end, end);
|
|
73
|
+
return buildModule([f], shared);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// move(start, end, unit, word, shift, idxS, loS, hiS, idxD, loD, hiD, offPtr, carryLo):
|
|
77
|
+
// each row goes to off[unit][its bucket]++ (offsets worked out on the main
|
|
78
|
+
// thread, so the sort stays stable and no two units write the same place).
|
|
79
|
+
export function moveModule(shared) {
|
|
80
|
+
const f = new Fn('k', Array(13).fill(I32), []);
|
|
81
|
+
const [start, stop, unit, word, shift, idxS, loS, hiS, idxD, loD, hiD, offPtr, carryLo] = [...Array(13).keys()];
|
|
82
|
+
const i = f.local(I32), ob = f.local(I32), a = f.local(I32), p = f.local(I32), l = f.local(I32), h = f.local(I32);
|
|
83
|
+
f.emit(get(offPtr), get(unit), i32c(BUCKETS * 4), i32mul, i32add, set(ob), get(start), set(i));
|
|
84
|
+
f.emit(block, loop, get(i), get(stop), i32geu, brIf(1));
|
|
85
|
+
f.emit(addr4(loS, i), I32_LOAD, set(l), addr4(hiS, i), I32_LOAD, set(h));
|
|
86
|
+
f.emit(get(h), get(l), get(word), SELECT, get(shift), I32_SHR_U, i32c(BUCKETS - 1), i32and, i32c(2), I32_SHL, get(ob), i32add, set(a));
|
|
87
|
+
f.emit(get(a), I32_LOAD, set(p), get(a), get(p), i32c(1), i32add, I32_STORE);
|
|
88
|
+
f.emit(addr4(idxD, p), addr4(idxS, i), I32_LOAD, I32_STORE);
|
|
89
|
+
f.emit(get(carryLo), IF, addr4(loD, p), get(l), I32_STORE, end);
|
|
90
|
+
f.emit(addr4(hiD, p), get(h), I32_STORE);
|
|
91
|
+
f.emit(get(i), i32c(1), i32add, set(i), br(0), end, end);
|
|
92
|
+
return buildModule([f], shared);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// gather(start, end, unit, src, perm, dst): dst[i] = src[perm[i]], for
|
|
96
|
+
// elements of 8, 4, 2 or 1 bytes (float64; float32 or packed i32; u16; u8):
|
|
97
|
+
// bits are copied as they are, so packed columns stay packed.
|
|
98
|
+
const GATHER = {
|
|
99
|
+
8: { sh: 3, load: [0x29, 3, 0], store: [0x37, 3, 0] }, // i64.load / store
|
|
100
|
+
4: { sh: 2, load: [0x28, 2, 0], store: [0x36, 2, 0] }, // i32
|
|
101
|
+
2: { sh: 1, load: [0x2f, 1, 0], store: [0x3b, 1, 0] }, // i32.load16_u / store16
|
|
102
|
+
1: { sh: 0, load: [0x2d, 0, 0], store: [0x3a, 0, 0] }, // i32.load8_u / store8
|
|
103
|
+
};
|
|
104
|
+
export function gatherModule(bytes, shared) {
|
|
105
|
+
if (typeof bytes === 'string') bytes = bytes === 'f32' ? 4 : 8;
|
|
106
|
+
const { sh, load, store } = GATHER[bytes];
|
|
107
|
+
const f = new Fn('k', [I32, I32, I32, I32, I32, I32], []);
|
|
108
|
+
const [start, stop, , src, perm, dst] = [0, 1, 2, 3, 4, 5];
|
|
109
|
+
const i = f.local(I32);
|
|
110
|
+
f.emit(get(start), set(i));
|
|
111
|
+
f.emit(block, loop, get(i), get(stop), i32geu, brIf(1));
|
|
112
|
+
f.emit(get(i), i32c(sh), I32_SHL, get(dst), i32add);
|
|
113
|
+
f.emit(addr4(perm, i), I32_LOAD, i32c(sh), I32_SHL, get(src), i32add, load);
|
|
114
|
+
f.emit(store);
|
|
115
|
+
f.emit(get(i), i32c(1), i32add, set(i), br(0), end, end);
|
|
116
|
+
return buildModule([f], shared);
|
|
117
|
+
}
|