@secam/pgsql-ast-parser
Version:
Fork of pgsql-ast-parser Simple Postgres SQL parser/modifier for pg-mem
1,064 lines (974 loc) • 30.2 kB
text/typescript
import 'mocha';
import 'chai';
import { checkSelect, checkInvalid, columns, ref, star, tbl, name, qname, checkStatement, int, binary } from './spec-utils';
import { JoinType, SelectStatement } from './ast';
describe('Select statements', () => {
// yea... thats a valid query. Try it oO'
checkSelect(['select'], {
type: 'select',
});
checkSelect(['select 42', 'select(42)'], {
type: 'select',
columns: columns({
type: 'integer',
value: 42
}),
});
function aliased(alias: string): SelectStatement {
return {
type: 'select',
columns: [{
expr: {
type: 'integer',
value: 42
},
alias: { name: alias },
}],
};
}
// bugfix
checkSelect(['select 42 as primary'], aliased('primary'));
checkSelect(['select 42 as unique'], aliased('unique'));
checkSelect(['select count(*)'], {
type: 'select',
columns: columns({
type: 'call',
function: { name: 'count' },
args: [{ type: 'ref', name: '*' }],
})
});
checkSelect(['select 42, 53', 'select 42,53', 'select(42),53'], {
type: 'select',
columns: columns({
type: 'integer',
value: 42
}, {
type: 'integer',
value: 53
}),
});
checkSelect(['select * from test', 'select*from"test"', 'select* from"test"', 'select *from"test"', 'select*from "test"', 'select * from "test"'], {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' })
});
checkSelect(['select * from current_schema()', 'select * from current_schema ( )'], {
type: 'select',
from: [{ type: 'call', function: { name: 'current_schema' }, args: [] }],
columns: columns({ type: 'ref', name: '*' })
});
checkSelect(['select a as a1, b as b1 from test', 'select a a1,b b1 from test', 'select a a1 ,b b1 from test'], {
type: 'select',
from: [tbl('test')],
columns: [{
expr: { type: 'ref', name: 'a' },
alias: { name: 'a1' },
}, {
expr: { type: 'ref', name: 'b' },
alias: { name: 'b1' },
}],
});
checkSelect(['select * from db.test'], {
type: 'select',
from: [{ type: 'table', name: qname('test', 'db') }],
columns: columns({ type: 'ref', name: '*' }),
});
checkSelect(['select * from test limit 5', 'select * from test fetch first 5 row only', 'select * from test fetch next 5 rows only'], {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
limit: {
limit: { type: 'integer', value: 5 }
},
});
checkSelect(['select * from unnest(generate_series(1, 10)) AS test(num)'], {
type: 'select',
from: [{
type: 'call',
function: { name: 'unnest' },
alias: {
name: 'test',
columns: [
{ name: 'num' },
],
},
args: [
{
type: 'call',
function: { name: 'generate_series' },
args: [
{ type: 'integer', value: 1 },
{ type: 'integer', value: 10 },
],
},
],
}],
columns: columns({ type: 'ref', name: '*' }),
});
checkSelect(['select * from unnest(ARRAY[\'foo\', \'bar\', \'baz\']) with ordinality AS test(thing, num)'], {
type: 'select',
from: [{
type: 'call',
function: { name: 'unnest' },
withOrdinality: true,
alias: {
name: 'test',
columns: [
{ name: 'thing' },
{ name: 'num' },
],
},
args: [
{
type: 'array',
expressions: [
{ type: 'string', value: 'foo' },
{ type: 'string', value: 'bar' },
{ type: 'string', value: 'baz' },
]
}
],
}],
columns: columns({ type: 'ref', name: '*' }),
});
checkSelect(['select t.* from things AS t join unnest(ARRAY[\'foo\', \'bar\']) with ordinality AS f(thing, ord) using (thing) order by f.ord'], {
type: 'select',
from: [
{
type: 'table',
name: { name: 'things', alias: 't' }
},
{
type: 'call',
function: { name: 'unnest' },
join: {
type: 'INNER JOIN',
using: [
{ name: 'thing' }
],
},
withOrdinality: true,
alias: {
name: 'f',
columns: [
{ name: 'thing' },
{ name: 'ord' },
],
},
args: [
{
type: 'array',
expressions: [
{ type: 'string', value: 'foo' },
{ type: 'string', value: 'bar' },
],
}
],
}
],
columns: columns({
type: 'ref',
table: { name: 't' },
name: '*',
}),
orderBy: [
{
by: {
type: 'ref',
table: { name: 'f' },
name: 'ord',
}
}
]
});
checkSelect(['select * from test limit 0'], {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
limit: {
limit: { type: 'integer', value: 0 }
},
});
checkSelect(['select * from test limit 5 offset 3', 'select * from test offset 3 limit 5', 'select * from test offset 3 rows fetch first 5 rows only'], {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
limit: {
limit: { type: 'integer', value: 5 }
, offset: { type: 'integer', value: 3 },
},
});
checkSelect(['select * from test limit $1 offset $2'], {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
limit: {
limit: { type: 'parameter', name: '$1' },
offset: { type: 'parameter', name: '$2' },
},
});
checkSelect(['select * from test offset 3', 'select * from test offset 3 rows'], {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
limit: {
offset: { type: 'integer', value: 3 },
},
});
checkSelect(['select * from test order by a asc limit 3'], {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
limit: {
limit: { type: 'integer', value: 3 }
},
orderBy: [{
by: { type: 'ref', name: 'a' },
order: 'ASC',
}]
});
checkSelect(['select * from test order by a limit 3'], {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
limit: {
limit: { type: 'integer', value: 3 }
},
orderBy: [{
by: { type: 'ref', name: 'a' },
}]
});
checkSelect(['select * from test order by a asc, b desc'], {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
orderBy: [{
by: { type: 'ref', name: 'a' },
order: 'ASC',
}, {
by: { type: 'ref', name: 'b' },
order: 'DESC',
}]
});
checkSelect(['select * from test order by a asc nulls first'], {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
orderBy: [{
by: { type: 'ref', name: 'a' },
order: 'ASC',
nulls: 'FIRST',
}]
});
checkSelect(['select * from test order by a asc nulls last'], {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
orderBy: [{
by: { type: 'ref', name: 'a' },
order: 'ASC',
nulls: 'LAST',
}]
});
checkSelect(['select a.*, b.*'], {
type: 'select',
columns: columns({
type: 'ref',
name: '*',
table: { name: 'a' },
}, {
type: 'ref',
name: '*',
table: { name: 'b' },
})
});
checkSelect(['select a, b'], {
type: 'select',
columns: columns(
{ type: 'ref', name: 'a' },
{ type: 'ref', name: 'b' })
});
checkSelect(['select * from test a where a.b > 42' // yea yea, all those are valid & equivalent..
, 'select*from test"a"where a.b > 42'
, 'select*from test as"a"where a.b > 42'
, 'select*from test as a where a.b > 42'], {
type: 'select',
from: [{ type: 'table', name: { name: 'test', alias: 'a' } }],
columns: columns({ type: 'ref', name: '*' }),
where: {
type: 'binary',
op: '>',
left: {
type: 'ref',
table: { name: 'a' },
name: 'b',
},
right: {
type: 'integer',
value: 42,
},
}
});
checkInvalid('select "*" from test');
checkInvalid('select (*) from test');
checkInvalid('select ("*") from test');
checkInvalid('select * from (test)');
checkInvalid('select * from (select id from test)'); // <== missing alias
checkInvalid('select * from sum(DISTINCT whatever)');
checkSelect('select * from (select id from test) d', {
type: 'select',
columns: columns({ type: 'ref', name: '*' }),
from: [{
type: 'statement',
statement: {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: 'id' }),
},
alias: 'd',
}]
})
checkSelect(['select * from test group by grp', 'select * from test group by (grp)'], {
type: 'select',
columns: columns({ type: 'ref', name: '*' }),
from: [tbl('test')],
groupBy: [{ type: 'ref', name: 'grp' }]
})
checkSelect(['select * from test group by grp having a > 42'], {
type: 'select',
columns: columns({ type: 'ref', name: '*' }),
from: [tbl('test')],
groupBy: [{ type: 'ref', name: 'grp' }],
having: {
type: 'binary',
op: '>',
left: ref('a'),
right: { type: 'integer', value: 42 },
},
})
checkSelect(['select * from test group by a,b', 'select * from test group by (a,b)'], {
type: 'select',
columns: columns({ type: 'ref', name: '*' }),
from: [tbl('test')],
groupBy: [
{ type: 'ref', name: 'a' },
{ type: 'ref', name: 'b' }
]
})
function buildJoin(t: JoinType): SelectStatement {
return {
type: 'select',
columns: columns({ type: 'ref', name: '*' }),
from: [tbl('ta'), {
type: 'table',
name: name('tb'),
join: {
type: t,
on: {
type: 'binary',
op: '=',
left: {
type: 'ref',
table: { name: 'ta' },
name: 'id',
},
right: {
type: 'ref',
table: { name: 'tb' },
name: 'id',
},
}
}
}]
}
}
checkInvalid('select * from ta full inner join tb on ta.id=tb.id');
checkInvalid('select * from ta left inner join tb on ta.id=tb.id');
checkInvalid('select * from ta right inner join tb on ta.id=tb.id');
checkInvalid('select * from ta cross inner join tb on ta.id=tb.id');
checkInvalid('select * from ta cross outer join tb on ta.id=tb.id');
checkSelect(['select * from ta join tb on ta.id=tb.id'
, 'select * from ta inner join tb on ta.id=tb.id'
, 'select * from (ta join tb on ta.id=tb.id)'
, 'select * from (((ta join tb on ta.id=tb.id)))']
, buildJoin('INNER JOIN'));
checkSelect(['select * from ta left join tb on ta.id=tb.id'
, 'select * from ta left outer join tb on ta.id=tb.id']
, buildJoin('LEFT JOIN'));
checkSelect(['select * from ta right join tb on ta.id=tb.id'
, 'select * from ta right outer join tb on ta.id=tb.id']
, buildJoin('RIGHT JOIN'));
checkSelect(['select * from ta full join tb on ta.id=tb.id'
, 'select * from ta full outer join tb on ta.id=tb.id']
, buildJoin('FULL JOIN'));
checkSelect('select * from ta cross join tb on ta.id=tb.id'
, buildJoin('CROSS JOIN'));
// implicit cross join
checkSelect('select * from ta, tb where ta.id=tb.id',
{
type: 'select',
columns: [{ expr: star }],
from: [
tbl('ta'),
tbl('tb'),
],
where: {
type: 'binary',
op: '=',
left: {
type: 'ref',
table: { name: 'ta' },
name: 'id',
},
right: {
type: 'ref',
table: { name: 'tb' },
name: 'id',
}
}
}
);
// implicit cross join multiple tables
checkSelect('select * from ta, tb, tc, td',
{
type: 'select',
columns: [{ expr: star }],
from: [
tbl('ta'),
tbl('tb'),
tbl('tc'),
tbl('td'),
]
}
);
// mixed join
checkSelect('select * from ta, tb cross join tc, (select * from td) as te', {
type: 'select',
columns: [{ expr: star }],
from: [
tbl('ta'),
tbl('tb'),
{
type: 'table',
name: name('tc'),
join: {
type: 'CROSS JOIN',
},
},
{
type: 'statement',
alias: 'te',
statement: {
type: 'select',
columns: [{ expr: star }],
from: [tbl('td')],
},
},
],
});
// double join with and without parens
checkSelect([`select * from ta cross join tb cross join tc`
, `select * from (ta cross join tb) cross join tc`]
, {
type: 'select',
columns: [{ expr: star }],
from: [
tbl('ta'),
{
type: 'table',
name: name('tb'),
join: {
type: 'CROSS JOIN',
},
},
{
type: 'table',
name: name('tc'),
join: {
type: 'CROSS JOIN',
},
}
],
}
);
// join, then implicit cross join
checkSelect(`select * from (ta cross join tb), tc`
, {
type: 'select',
columns: [{ expr: star }],
from: [
tbl('ta'),
{
type: 'table',
name: name('tb'),
join: {
type: 'CROSS JOIN',
},
},
tbl('tc'),
],
}
);
checkSelect(`SELECT *
FROM STUD_ASS_PROGRESS
LEFT JOIN ACCURACY
USING("studentId")`, {
type: 'select',
columns: [{ expr: star }],
from: [tbl('stud_ass_progress'),
{
type: 'table',
name: name('accuracy'),
join: {
type: 'LEFT JOIN',
using: [{ name: 'studentId' }],
}
}
]
});
checkSelect(`
select * from test
inner join lateral (
select * from test2
where test2.foo = test1.bar
) test2_inner on true
`, {
type: 'select',
columns: [{ expr: star }],
from: [tbl('test'),
{
alias: "test2_inner",
join: {
on: {
type: "boolean",
value: true
},
type: "INNER JOIN",
},
statement: {
columns: [
{
expr: {
name: "*",
type: "ref"
}
}
],
from: [
{
name: {
name: "test2"
},
type: "table"
}
],
type: "select",
where: {
left: {
name: "foo",
table: {
name: "test2"
},
type: "ref"
},
op: "=",
right: {
name: "bar",
table: {
name: "test1"
},
type: "ref",
},
type: "binary"
}
},
type: "statement",
lateral: true,
}
]
});
checkSelect(`
SELECT m.name AS mname, pname
FROM manufacturers m, LATERAL get_product_names(m.id) pname;
`, {
"columns": [
{
"expr": {
"type": "ref",
"table": {
"name": "m"
},
"name": "name"
},
"alias": {
"name": "mname"
}
},
{
"expr": {
"type": "ref",
"name": "pname"
}
}
],
"from": [
{
"type": "table",
"name": {
"name": "manufacturers",
"alias": "m"
}
},
{
"type": "call",
"function": {
"name": "get_product_names"
},
"args": [
{
"type": "ref",
"table": {
"name": "m"
},
"name": "id"
}
],
"lateral": true,
"alias": {
"name": "pname"
}
}
],
"type": "select"
});
checkSelect(['select current_schema()'], {
type: 'select',
columns: [{
expr: {
type: 'call',
function: {
name: 'current_schema',
},
args: [],
}
}]
})
checkSelect(`select '1'::double precision`, {
type: 'select',
columns: [{
expr: {
type: 'cast',
operand: { type: 'string', value: '1' },
to: { name: 'double precision' },
}
}]
});
checkSelect(`select '1'::"double precision"`, {
type: 'select',
columns: [{
expr: {
type: 'cast',
operand: { type: 'string', value: '1' },
to: { name: 'double precision', doubleQuoted: true },
}
}]
});
checkSelect(`select '1'::double precision x`, {
type: 'select',
columns: [{
alias: { name: 'x' },
expr: {
type: 'cast',
operand: { type: 'string', value: '1' },
to: { name: 'double precision' },
}
}]
});
checkSelect(['select now()::time without time zone'], {
type: 'select',
columns: [{
expr: {
type: 'cast',
operand: {
type: 'call',
function: { name: 'now' },
args: [],
},
to: { name: 'time without time zone' },
}
}]
})
checkSelect(['select distinct a from test'], {
type: 'select',
from: [tbl('test')],
distinct: 'distinct',
columns: columns({ type: 'ref', name: 'a' }),
});
checkSelect(['select distinct on (a) a from test'], {
type: 'select',
from: [tbl('test')],
distinct: [{ type: 'ref', name: 'a' }],
columns: columns({ type: 'ref', name: 'a' }),
});
checkSelect(['select distinct on (a, b) a from test'], {
type: 'select',
from: [tbl('test')],
distinct: [{ type: 'ref', name: 'a' }, { type: 'ref', name: 'b' }],
columns: columns({ type: 'ref', name: 'a' }),
});
checkSelect(['select count(distinct("userId")) from photo'], {
type: 'select',
from: [tbl('photo')],
columns: columns({
type: 'call',
function: { name: 'count' },
distinct: 'distinct',
args: [ref('userId')],
})
});
checkSelect(['select max(distinct("userId")) from photo'], {
type: 'select',
from: [tbl('photo')],
columns: columns({
type: 'call',
function: { name: 'max' },
distinct: 'distinct',
args: [ref('userId')],
})
});
checkSelect(['select all a from test'], {
type: 'select',
from: [tbl('test')],
distinct: 'all',
columns: columns({ type: 'ref', name: 'a' }),
});
checkStatement(`VALUES (1, 1+1), (3, 4)`, {
type: 'values',
values: [
[int(1), binary(int(1), '+', int(1))],
[int(3), int(4)],
]
})
checkSelect([`select * from (values (1, 'one'), (2, 'two')) as vals (num, letter)`], {
type: 'select',
from: [{
type: 'statement',
statement: {
type: 'values',
values: [
[{ type: 'integer', value: 1 }, { type: 'string', value: 'one' }],
[{ type: 'integer', value: 2 }, { type: 'string', value: 'two' }],
],
},
alias: 'vals',
columnNames: [{ name: 'num' }, { name: 'letter' }],
}],
columns: columns({ type: 'ref', name: '*' })
});
checkSelect([`select * from (values (1, 'one'), (2, 'two')) as vals`], {
type: 'select',
from: [{
type: 'statement',
statement: {
type: 'values',
values: [
[{ type: 'integer', value: 1 }, { type: 'string', value: 'one' }],
[{ type: 'integer', value: 2 }, { type: 'string', value: 'two' }],
],
},
alias: 'vals',
}],
columns: columns({ type: 'ref', name: '*' })
});
checkSelect([`SELECT t1.id FROM (ta t1 JOIN tb t2 ON ((t1.id = t2.n)));`], {
type: 'select',
columns: columns({
type: 'ref',
name: 'id',
table: { name: 't1' },
}),
from: [{
type: 'table',
name: {
name: 'ta',
alias: 't1',
},
}, {
type: 'table',
name: {
name: 'tb',
alias: 't2',
},
join: {
type: 'INNER JOIN',
on: {
type: 'binary',
op: '=',
left: {
type: 'ref',
table: { name: 't1' },
name: 'id',
},
right: {
type: 'ref',
table: { name: 't2' },
name: 'n',
},
}
}
}]
})
checkSelect([`select * from concat('a', 'b')`], {
type: 'select',
from: [{
type: 'call',
function: { name: 'concat' },
args: [
{ type: 'string', value: 'a' },
{ type: 'string', value: 'b' },
]
}],
columns: columns({ type: 'ref', name: '*' }),
})
checkSelect([`select * from concat('a') as a join concat('b') as b on b=a`], {
type: 'select',
from: [{
type: 'call',
function: { name: 'concat' },
alias: { name: 'a' },
args: [
{ type: 'string', value: 'a' },
]
}, {
type: 'call',
function: { name: 'concat' },
args: [
{ type: 'string', value: 'b' },
],
alias: { name: 'b' },
join: {
type: 'INNER JOIN',
on: {
type: 'binary',
op: '=',
left: {
type: 'ref',
name: 'b',
},
right: {
type: 'ref',
name: 'a',
},
}
},
}],
columns: columns({ type: 'ref', name: '*' }),
})
checkSelect([`select * from concat('a', 'b') as tbl`], {
type: 'select',
from: [{
type: 'call',
function: { name: 'concat' },
alias: { name: 'tbl' },
args: [
{ type: 'string', value: 'a' },
{ type: 'string', value: 'b' },
]
}],
columns: columns({ type: 'ref', name: '*' }),
})
checkSelect([`select 1 from fn() alias`, `select 1 from fn() as alias`], {
type: 'select',
from: [{
type: 'call',
function: { name: 'fn' },
alias: { name: 'alias' },
args: [],
}],
columns: columns({ type: 'integer', value: 1 }),
})
checkSelect(`SELECT
(
WITH x AS (select val from example)
SELECT lower(val)
FROM x
)`, {
type: 'select',
columns: columns({
type: 'with',
bind: [{
alias: { name: 'x' },
statement: {
type: 'select',
columns: [{ expr: { type: 'ref', name: 'val' } }],
from: [tbl('example')],
},
}],
in: {
type: 'select',
columns: [{
expr: {
type: 'call',
function: { name: 'lower' },
args: [{ type: 'ref', name: 'val' }],
}
}],
from: [tbl('x')],
}
}),
});
checkSelect('select * from test for update', {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
for: {
type: 'update',
}
});
checkSelect('select * from test for no key update', {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
for: {
type: 'no key update',
}
});
checkSelect('select * from test for share', {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
for: {
type: 'share',
}
});
checkSelect('select * from test for key share', {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
for: {
type: 'key share',
}
});
checkSelect('select * from test for key share nowait', {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
for: {
type: 'key share',
},
skip: {
type: 'nowait',
}
});
checkSelect('select * from test for key share skip locked', {
type: 'select',
from: [tbl('test')],
columns: columns({ type: 'ref', name: '*' }),
for: {
type: 'key share',
},
skip: {
type: 'skip locked',
}
});
});