Read data¶
Select rows from a table¶
Let's create a table and insert some data:
We can run SELECT
queries using exec
. Here are a few examples:
console.log("Raw data:", await skdb.exec("SELECT * FROM test"));
console.log("\nNow we'll pretty print results.\n");
console.log("Choose columns with a simple filter");
console.table(await skdb.exec("SELECT t FROM test WHERE n < 2"));
console.log("Simple aggregation");
console.table(await skdb.exec(`
SELECT
t,
sum(n) AS total
FROM
test
GROUP BY
t`));
console.log("Nested query");
console.table(await skdb.exec(`
SELECT
sum(derived) AS total
FROM
(
SELECT
cast(n AS float) + f AS derived
FROM
test
)`));
console.log("Union");
console.table(await skdb.exec(`
SELECT
t,
n
FROM
test
WHERE
t = 'foo'
UNION
ALL
SELECT
t,
n
FROM
test
WHERE
n % 2 = 0`));
Raw data: SKDBTable(3) [
{ id: '3g476VldeHN8sJBsr5brOM6UIp1', n: 1, f: 0.2, t: 'bar' },
{ id: '3g476ZNkj9-b3vj6issFQDLSux1', n: 0, f: 0.1, t: 'foo' },
{ id: '3g476eop_aeUKPC_AhCVN3cE-XE', n: 2, f: 0.3, t: 'foo' }
]
Now we'll pretty print results.
Choose columns with a simple filter
┌─────────┬───────┐
│ (index) │ t │
├─────────┼───────┤
│ 0 │ 'bar' │
│ 1 │ 'foo' │
└─────────┴───────┘
Simple aggregation
┌─────────┬───────┬───────┐
│ (index) │ t │ total │
├─────────┼───────┼───────┤
│ 0 │ 'bar' │ 1 │
│ 1 │ 'foo' │ 2 │
└─────────┴───────┴───────┘
Nested query
┌─────────┬───────┐
│ (index) │ total │
├─────────┼───────┤
│ 0 │ 3.6 │
└─────────┴───────┘
Union
┌─────────┬───────┬───┐
│ (index) │ t │ n │
├─────────┼───────┼───┤
│ 0 │ 'foo' │ 0 │
│ 1 │ 'foo' │ 0 │
│ 2 │ 'foo' │ 2 │
│ 3 │ 'foo' │ 2 │
└─────────┴───────┴───┘
Parameterize queries¶
Query parameters allow you to easily include application data in your queries. This is convenient but also ensures that the input data is not interpreted as SQL, avoiding SQL injection attacks.
Recommendation: Always use parameterized queries to avoid SQL injection vulnerabilities.
Continuing from our basic SELECT
statements example,
this illustrates the use of query parameters.
const maxN = 2;
const interestingText = "foo";
const rows = await skdb.exec(
"SELECT * FROM test WHERE n < @maxN AND t = @interestingText",
{ maxN, interestingText }
);
console.log(rows);
Parameters are indicated with the '@' symbol in the SQL query. They
are replaced with values from the application as per the map that is
passed to exec
.
Work with an SKDBTable
object¶
When SKDB provides the results of a query, it does so using an
SKDBTable
object. exec
returns an SKDBTable
,
watch
and watchChanges
invoke callbacks with an SKDBTable
.
SKDBTable
objects are Array objects -- they are arrays of JavaScript
records corresponding to the select parameters -- so any of the usual
JavaScript array methods will work. e.g. you can index, use length
,
concat()
and so on.
SKDBTable
objects also support the following convenience methods
when working with table data:
scalarValue(): any | undefined
- Get the only value, from a single column, single row table. Or
undefined
if there are no rows or no columns.
- Get the only value, from a single column, single row table. Or
onlyRow(): Record<string, any>
- Get the only row, or throw if there are no rows or multiple rows.
firstRow(): Record<string, any> | undefined
- Get the first row or
undefined
if there are no rows.
- Get the first row or
lastRow(): Record<string, any> | undefined
- Get the last row or
undefined
if there are no rows.
- Get the last row or
onlyColumn(): any[]
- Extract the only column as an array of values. Returns an empty array if there are no rows or columns, throws if there are multiple columns.
column(name: string): any[]
- Extract the column named
name
as an array of values.
- Extract the column named
Continuing from our basic SELECT
statements example,
this illustrates some of the methods on SKDBTable
.
const fullTable = await skdb.exec("SELECT * FROM test");
const oneCol = await skdb.exec("SELECT id FROM test");
const oneRow = await skdb.exec("SELECT * FROM test WHERE t = 'bar'");
const oneValue = await skdb.exec("SELECT count(*) as n FROM test");
console.log("Full table:", fullTable);
console.log("Only column:", oneCol.onlyColumn());
console.log("Only row:", oneRow.onlyRow());
console.log("Only value:", oneValue.scalarValue());
console.log("'f' col:", fullTable.column('f'));
Full table: SKDBTable(3) [
{ id: '3g475ZhNXCKN09Wu5mq2YwjBLFE', n: 1, f: 0.2, t: 'bar' },
{ id: '3g475hqMhIc_Y8LQbpWvmlQxWq0', n: 0, f: 0.1, t: 'foo' },
{ id: '3g475iUoTDY2YltoLjelAGeSCiE', n: 2, f: 0.3, t: 'foo' }
]
Only column: SKDBTable(3) [
'3g475ZhNXCKN09Wu5mq2YwjBLFE',
'3g475hqMhIc_Y8LQbpWvmlQxWq0',
'3g475iUoTDY2YltoLjelAGeSCiE'
]
Only row: { id: '3g475ZhNXCKN09Wu5mq2YwjBLFE', n: 1, f: 0.2, t: 'bar' }
Only value: 3
'f' col: SKDBTable(3) [ 0.2, 0.1, 0.3 ]
Subscribe to SQL queries and react to updates¶
watch
allows you to run any SQL query that SKDB supports and get the
whole result set each time it changes.
This is very useful when wanting to rebuild views over the whole result set, e.g. when re-rendering react components or re-writing documents into a cache.
You can watch a mirrored table to learn about changes other users are making in real time.
Thanks to SKDB's incremental engine, a watch
ed query is never
re-run. It is incrementally updated only when relevant data changes in
the database. Due to the incremental engine, watch
ed queries are
very efficient and you can run a large number of them at once.
Assuming we've created a 'test' table. Let's
see how watch
works:
const handle = await skdb.watch(
"select * from test where t = @search",
{ search: "foo" },
(table) => {
console.log("Update from watch:", table);
}
);
await skdb.exec("INSERT INTO test(t) VALUES ('foo')");
await skdb.exec("INSERT INTO test(t) VALUES ('bar')");
await skdb.exec("UPDATE test SET t = 'foo' WHERE t = 'bar'");
await skdb.exec("DELETE FROM TEST");
await handle.close();
Update from watch: SKDBTable(0) []
Update from watch: SKDBTable(1) [
{ id: '3g476V8MILv_R-Y01i4Hxu9hUI0', n: null, f: null, t: 'foo' }
]
Update from watch: SKDBTable(2) [
{ id: '3g476V8MILv_R-Y01i4Hxu9hUI0', n: null, f: null, t: 'foo' },
{ id: '3g476cdIUmUA5CTd_lMJCHxOQ47', n: null, f: null, t: 'foo' }
]
Update from watch: SKDBTable(0) []
Subscribe to SQL queries and monitor only the changes¶
watchChanges
allows you to run any SQL query that SKDB supports, get
the results, and then learn how the results are changing as the
underlying data changes.
You can use watchChanges
with a mirrored table to learn about
changes other users are making in real time.
Thanks to SKDB's incremental engine, a watched query is never re-run. It is incrementally updated only when relevant data changes in the database. Due to the incremental engine, watching for changes is very efficient and you can watch a large number of queries at once.
Assuming we've created a 'test' table. Let's
see how watchChanges
works:
await skdb.exec("INSERT INTO test(t) VALUES ('foo')");
const handle = await skdb.watchChanges(
"select id, t from test",
{},
(table) => {
console.log("Initial result set:", table);
console.log("");
},
(added, removed) => {
console.log("The query result has changed.");
console.log("Rows added:", added);
console.log("Rows removed:", removed);
console.log("");
}
);
await skdb.exec("UPDATE test SET t = 'bar' WHERE t = 'foo'");
await handle.close();
Initial result set: SKDBTable(1) [ { id: '3g477R_vQUOXHpKF1ILKxxiNCR0', t: 'foo' } ]
The query result has changed.
Rows added: SKDBTable(1) [ { id: '3g477R_vQUOXHpKF1ILKxxiNCR0', t: 'bar' } ]
Rows removed: SKDBTable(1) [ { id: '3g477R_vQUOXHpKF1ILKxxiNCR0', t: 'foo' } ]
Build reactive views¶
Reactive views materialize queries - they build and maintain a new read only table defined by any SQL query supported by SKDB. This allows the resulting table to be queried without recomputing the view constantly.
This allows you to very efficiently maintain expensive joins, filters, and aggregations, for example.
The SKDB engine was purpose built to maintain reactive views incrementally. As data is changed in source table(s) any dependent views are updated very efficiently. SKDB knows if the view is affected by the update and will update just the rows that need to be.
Let's assume that we have created a local table -- but this of course works for mirrored tables too.
We can build a reactive view using exec
:
await skdb.exec(
`CREATE REACTIVE VIEW test_totals AS
SELECT
t,
sum(n) AS total
FROM
test
GROUP BY
t;`
);
console.log("Schema with the reactive view:")
console.log(await skdb.schema());
await skdb.exec("INSERT INTO test (t, n) VALUES ('foo', 1), ('bar', 1), ('foo', 7)");
console.log("Querying the table:")
console.table(await skdb.exec("SELECT t, n FROM test"));
console.log("Querying the view:")
console.table(await skdb.exec("SELECT * FROM test_totals ORDER BY total"));
Schema with the reactive view:
CREATE TABLE test (
id TEXT PRIMARY KEY,
n INTEGER,
f FLOAT,
t TEXT
);
CREATE REACTIVE VIEW test_totals AS SELECT t, sum(n) AS total FROM test GROUP BY t;
Querying the table:
┌─────────┬───────┬───┐
│ (index) │ t │ n │
├─────────┼───────┼───┤
│ 0 │ 'bar' │ 1 │
│ 1 │ 'foo' │ 7 │
│ 2 │ 'foo' │ 1 │
└─────────┴───────┴───┘
Querying the view:
┌─────────┬───────┬───────┐
│ (index) │ t │ total │
├─────────┼───────┼───────┤
│ 0 │ 'bar' │ 1 │
│ 1 │ 'foo' │ 8 │
└─────────┴───────┴───────┘
Reactive views can be built on top of reactive views and the whole graph will be maintained incrementally.
You can use watch
and watchChanges
with a reactive view too.