Files
supabase/packages/pg-meta/test/query/table-row-query.test.ts
Ivan Vasilov 1cd1ebfc7f chire: Sort imports in all packages, cms, design-system and ui-library apps (#41610)
Sorted all imports in all packages, `cms`, `design-system` and
`ui-library` apps by running `pnpm format` on them.

All changes in this PR are done by the script.
2026-02-05 13:54:10 +01:00

1851 lines
58 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, test } from 'vitest'
import pgMeta from '../../src/index'
import { Filter, Sort } from '../../src/query'
import { getDefaultOrderByColumns, getTableRowsSql } from '../../src/query/table-row-query'
import { cleanupRoot, createTestDatabase } from '../db/utils'
beforeAll(async () => {
// Any global setup if needed
})
afterAll(async () => {
await cleanupRoot()
})
type TestDb = Awaited<ReturnType<typeof createTestDatabase>>
const withTestDatabase = (name: string, fn: (db: TestDb) => Promise<void>) => {
test(name, async () => {
const db = await createTestDatabase()
try {
await fn(db)
} finally {
await db.cleanup()
}
})
}
describe('Table Row Query', () => {
describe('getDefaultOrderByColumns', () => {
test('should return empty array when no primary keys and no columns exist', () => {
const result = getDefaultOrderByColumns({
primary_keys: [],
columns: [],
})
expect(result).toEqual([])
})
test('should exclude specified columns when determining default sort', () => {
const table = {
primary_keys: [{ name: 'id' }],
columns: [
{
name: 'id',
data_type: 'integer',
format: 'int4',
ordinal_position: 1,
},
{
name: 'name',
data_type: 'text',
format: 'text',
ordinal_position: 2,
},
],
} as any
const result = getDefaultOrderByColumns(table, { excludedColumns: ['id'] })
expect(result).toEqual(['name'])
})
})
describe('getTableRowsSql', () => {
withTestDatabase('should handle array of enums correctly', async (db) => {
// Create an enum type and a table with an array of that enum
await db.executeQuery(`
-- Create enum type
CREATE TYPE status_type AS ENUM ('pending', 'active', 'completed', 'canceled');
-- Create table with array of enums
CREATE TABLE test_enum_array (
id SERIAL PRIMARY KEY,
name TEXT,
status status_type, -- Regular enum column
history status_type[] -- Array of enums
);
-- Insert test data with various enum array values
INSERT INTO test_enum_array (name, status, history) VALUES
('Item 1', 'active', ARRAY['pending', 'active']::status_type[]),
('Item 2', 'completed', ARRAY['pending', 'active', 'completed']::status_type[]),
('Item 3', 'canceled', ARRAY['active', 'canceled']::status_type[]),
('Item 4', 'pending', NULL);
`)
// Get table metadata
const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
const tables = tablesZod.parse(await db.executeQuery(tablesSql))
const testTable = tables.find((table) => table.name === 'test_enum_array')
expect(testTable).toBeDefined()
// Generate SQL
const sql = getTableRowsSql({
table: testTable!,
page: 1,
limit: 10,
})
// Verify SQL generation with snapshot
expect(sql).toMatchInlineSnapshot(`
"with _base_query as (select * from public.test_enum_array order by test_enum_array.id asc nulls last limit 10 offset 0)
select id,case
when octet_length(name::text) > 10240
then left(name::text, 10240) || '...'
else name::text
end as name,status,
case
when octet_length(history::text) > 10240
then
case
when array_ndims(history) = 1
then
(select array_cat(history[1:50]::text[], array['...']::text[]))::text[]
else
history[1:50]::text[]
end
else history::text[]
end
from _base_query;"
`)
// Execute the generated SQL and verify the results
const queryResult = await db.executeQuery(sql)
expect(queryResult).toMatchInlineSnapshot(`
[
{
"history": [
"pending",
"active",
],
"id": 1,
"name": "Item 1",
"status": "active",
},
{
"history": [
"pending",
"active",
"completed",
],
"id": 2,
"name": "Item 2",
"status": "completed",
},
{
"history": [
"active",
"canceled",
],
"id": 3,
"name": "Item 3",
"status": "canceled",
},
{
"history": null,
"id": 4,
"name": "Item 4",
"status": "pending",
},
]
`)
// Test filtering on enum array values
const filtersStatusSQL = getTableRowsSql({
table: testTable!,
filters: [
{ column: 'status', operator: '=', value: `active` }, // Contains 'active' using text pattern matching
],
page: 1,
limit: 10,
})
expect(filtersStatusSQL).toMatchInlineSnapshot(`
"with _base_query as (select * from public.test_enum_array where status = 'active' order by test_enum_array.id asc nulls last limit 10 offset 0)
select id,case
when octet_length(name::text) > 10240
then left(name::text, 10240) || '...'
else name::text
end as name,status,
case
when octet_length(history::text) > 10240
then
case
when array_ndims(history) = 1
then
(select array_cat(history[1:50]::text[], array['...']::text[]))::text[]
else
history[1:50]::text[]
end
else history::text[]
end
from _base_query;"
`)
// Execute the filtered query
const filtersStatusResult = await db.executeQuery(filtersStatusSQL)
expect(filtersStatusResult).toMatchInlineSnapshot(`
[
{
"history": [
"pending",
"active",
],
"id": 1,
"name": "Item 1",
"status": "active",
},
]
`)
const filtersHistorySQL = getTableRowsSql({
table: testTable!,
filters: [
{ column: 'history', operator: '=', value: `ARRAY['active']::status_type[]` }, // Contains 'active' using text pattern matching
],
page: 1,
limit: 10,
})
expect(filtersHistorySQL).toMatchInlineSnapshot(`
"with _base_query as (select * from public.test_enum_array where history = ARRAY['active']::status_type[] order by test_enum_array.id asc nulls last limit 10 offset 0)
select id,case
when octet_length(name::text) > 10240
then left(name::text, 10240) || '...'
else name::text
end as name,status,
case
when octet_length(history::text) > 10240
then
case
when array_ndims(history) = 1
then
(select array_cat(history[1:50]::text[], array['...']::text[]))::text[]
else
history[1:50]::text[]
end
else history::text[]
end
from _base_query;"
`)
const filtersHistoryResult = await db.executeQuery(filtersHistorySQL)
expect(filtersHistoryResult).toMatchInlineSnapshot(`[]`)
})
withTestDatabase('should handle array columns correctly', async (db) => {
await db.executeQuery(`
CREATE TABLE test_array_table (
id SERIAL PRIMARY KEY,
name TEXT,
tags TEXT[] -- Array of text
);
-- Insert test data with array values
INSERT INTO test_array_table (name, tags) VALUES
('Item 1', ARRAY['tag1', 'tag2']),
('Item 2', ARRAY['tag3']),
('Item 3', ARRAY['tag1', 'tag4']);
`)
// Get table metadata
const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
const tables = tablesZod.parse(await db.executeQuery(tablesSql))
const testTable = tables.find((table) => table.name === 'test_array_table')
expect(testTable).toBeDefined()
// Generate SQL
const sql = getTableRowsSql({
table: testTable!,
page: 1,
limit: 10,
})
expect(sql).toMatchInlineSnapshot(`
"with _base_query as (select * from public.test_array_table order by test_array_table.id asc nulls last limit 10 offset 0)
select id,case
when octet_length(name::text) > 10240
then left(name::text, 10240) || '...'
else name::text
end as name,
case
when octet_length(tags::text) > 10240
then
case
when array_ndims(tags) = 1
then
(select array_cat(tags[1:50]::text[], array['...']::text[]))::text[]
else
tags[1:50]::text[]
end
else tags::text[]
end
from _base_query;"
`)
const queryResult = await db.executeQuery(sql)
expect(queryResult.length).toBe(3)
expect(queryResult).toMatchInlineSnapshot(`
[
{
"id": 1,
"name": "Item 1",
"tags": [
"tag1",
"tag2",
],
},
{
"id": 2,
"name": "Item 2",
"tags": [
"tag3",
],
},
{
"id": 3,
"name": "Item 3",
"tags": [
"tag1",
"tag4",
],
},
]
`)
})
withTestDatabase('should generate basic SELECT SQL for a table', async (db) => {
// Create test table and insert data
await db.executeQuery(`
CREATE TABLE test_sql_gen (
id SERIAL PRIMARY KEY,
name TEXT,
description TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- Insert test data
INSERT INTO test_sql_gen (name, description) VALUES
('Row 1', 'Description 1'),
('Row 2', 'Description 2'),
('Row 3', 'Description 3');
`)
// Get table metadata
const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
const tables = tablesZod.parse(await db.executeQuery(tablesSql))
const testTable = tables.find((table) => table.name === 'test_sql_gen')
expect(testTable).toBeDefined()
// Generate SQL
const sql = getTableRowsSql({
table: testTable!,
page: 1,
limit: 10,
})
// Verify SQL generation with snapshot
expect(sql).toMatchInlineSnapshot(
`
"with _base_query as (select * from public.test_sql_gen order by test_sql_gen.id asc nulls last limit 10 offset 0)
select id,case
when octet_length(name::text) > 10240
then left(name::text, 10240) || '...'
else name::text
end as name,case
when octet_length(description::text) > 10240
then left(description::text, 10240) || '...'
else description::text
end as description,created_at from _base_query;"
`
)
// E2E Test: Execute the SQL and verify results
const queryResult = await db.executeQuery(sql)
expect(queryResult.length).toBe(3)
expect(queryResult.map((row: any) => row.name)).toEqual(['Row 1', 'Row 2', 'Row 3'])
expect(queryResult.map((row: any) => row.description)).toEqual([
'Description 1',
'Description 2',
'Description 3',
])
})
withTestDatabase(
'should truncate large arrays to maxArraySize elements if their size is > maxCharacters',
async (db) => {
// Create test table with array column
await db.executeQuery(`
CREATE TABLE test_large_array_table (
id SERIAL PRIMARY KEY,
name TEXT,
large_array TEXT[] -- Will hold a very large array
);
-- Insert test data with a large array (>10KB)
-- Create an array with 1000 elements to ensure it exceeds 10KB
INSERT INTO test_large_array_table (name, large_array) VALUES
('Large Array Item', (SELECT array_agg('element_' || i) FROM generate_series(1, 1000) i)),
('Large Array Small items', (SELECT array_agg('' || i) FROM generate_series(1, 100) i)),
('Normal Array Item', ARRAY['tag1', 'tag2', 'tag3']);
`)
// Get table metadata
const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
const tables = tablesZod.parse(await db.executeQuery(tablesSql))
const testTable = tables.find((table) => table.name === 'test_large_array_table')
expect(testTable).toBeDefined()
// Generate SQL with lower maxCharacters and maxArraySize limits
const sql = getTableRowsSql({
table: testTable!,
page: 1,
limit: 10,
maxCharacters: 2048,
maxArraySize: 10,
})
// Verify the SQL contains the array truncation logic
expect(sql).toMatchInlineSnapshot(`
"with _base_query as (select * from public.test_large_array_table order by test_large_array_table.id asc nulls last limit 10 offset 0)
select id,case
when octet_length(name::text) > 2048
then left(name::text, 2048) || '...'
else name::text
end as name,
case
when octet_length(large_array::text) > 2048
then
case
when array_ndims(large_array) = 1
then
(select array_cat(large_array[1:10]::text[], array['...']::text[]))::text[]
else
large_array[1:10]::text[]
end
else large_array::text[]
end
from _base_query;"
`)
// Execute the SQL and verify results
const queryResult = await db.executeQuery(sql)
expect(queryResult).toMatchInlineSnapshot(`
[
{
"id": 1,
"large_array": [
"element_1",
"element_2",
"element_3",
"element_4",
"element_5",
"element_6",
"element_7",
"element_8",
"element_9",
"element_10",
"...",
],
"name": "Large Array Item",
},
{
"id": 2,
"large_array": [
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
"17",
"18",
"19",
"20",
"21",
"22",
"23",
"24",
"25",
"26",
"27",
"28",
"29",
"30",
"31",
"32",
"33",
"34",
"35",
"36",
"37",
"38",
"39",
"40",
"41",
"42",
"43",
"44",
"45",
"46",
"47",
"48",
"49",
"50",
"51",
"52",
"53",
"54",
"55",
"56",
"57",
"58",
"59",
"60",
"61",
"62",
"63",
"64",
"65",
"66",
"67",
"68",
"69",
"70",
"71",
"72",
"73",
"74",
"75",
"76",
"77",
"78",
"79",
"80",
"81",
"82",
"83",
"84",
"85",
"86",
"87",
"88",
"89",
"90",
"91",
"92",
"93",
"94",
"95",
"96",
"97",
"98",
"99",
"100",
],
"name": "Large Array Small items",
},
{
"id": 3,
"large_array": [
"tag1",
"tag2",
"tag3",
],
"name": "Normal Array Item",
},
]
`)
}
)
withTestDatabase(
'should truncate large arrays of jsonb and json to maxArraySize elements if their size is > maxCharacters',
async (db) => {
// Create test table with array column
await db.executeQuery(`
CREATE TABLE test_large_array_table (
id SERIAL PRIMARY KEY,
name TEXT,
large_array_jsonb jsonb[],
large_array_json json[]
);
-- Insert test data with a large array (>10KB)
-- Create arrays with JSON objects
INSERT INTO test_large_array_table (name, large_array_jsonb, large_array_json) VALUES
(
'Large Array Item',
(SELECT array_agg(jsonb_build_object(
'id', i,
'name', 'element_' || i,
'data', jsonb_build_object('value', i * 10, 'active', true)
)) FROM generate_series(1, 1000) i),
(SELECT array_agg(json_build_object(
'id', i,
'name', 'element_' || i,
'data', json_build_object('value', i * 10, 'active', true)
)) FROM generate_series(1, 1000) i)
),
(
'Large Array Small items',
(SELECT array_agg(jsonb_build_object(
'id', i,
'value', i
)) FROM generate_series(1, 100) i),
(SELECT array_agg(json_build_object(
'id', i,
'value', i
)) FROM generate_series(1, 100) i)
),
(
'Normal Array Item',
ARRAY[
'{"id": 1, "tag": "tag1"}'::jsonb,
'{"id": 2, "tag": "tag2"}'::jsonb,
'{"id": 3, "tag": "tag3"}'::jsonb
],
ARRAY[
'{"id": 1, "tag": "tag1"}'::json,
'{"id": 2, "tag": "tag2"}'::json,
'{"id": 3, "tag": "tag3"}'::json
]
);
`)
// Get table metadata
const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
const tables = tablesZod.parse(await db.executeQuery(tablesSql))
const testTable = tables.find((table) => table.name === 'test_large_array_table')
expect(testTable).toBeDefined()
// Generate SQL with lower maxCharacters and maxArraySize limits
const sql = getTableRowsSql({
table: testTable!,
page: 1,
limit: 10,
maxCharacters: 2048,
maxArraySize: 10,
})
// Verify the SQL contains the array truncation logic
expect(sql).toMatchInlineSnapshot(`
"with _base_query as (select * from public.test_large_array_table order by test_large_array_table.id asc nulls last limit 10 offset 0)
select id,case
when octet_length(name::text) > 2048
then left(name::text, 2048) || '...'
else name::text
end as name,
case
when octet_length(large_array_jsonb::text) > 2048
then
case
when array_ndims(large_array_jsonb) = 1
then
(select array_cat(large_array_jsonb[1:10]::jsonb[], array['{"truncated": true}'::json]::jsonb[]))::jsonb[]
else
large_array_jsonb[1:10]::jsonb[]
end
else large_array_jsonb::jsonb[]
end
,
case
when octet_length(large_array_json::text) > 2048
then
case
when array_ndims(large_array_json) = 1
then
(select array_cat(large_array_json[1:10]::json[], array['{"truncated": true}'::json]::json[]))::json[]
else
large_array_json[1:10]::json[]
end
else large_array_json::json[]
end
from _base_query;"
`)
// Execute the SQL and verify results
const queryResult = await db.executeQuery(sql)
expect(queryResult).toMatchInlineSnapshot(`
[
{
"id": 1,
"large_array_json": [
{
"data": {
"active": true,
"value": 10,
},
"id": 1,
"name": "element_1",
},
{
"data": {
"active": true,
"value": 20,
},
"id": 2,
"name": "element_2",
},
{
"data": {
"active": true,
"value": 30,
},
"id": 3,
"name": "element_3",
},
{
"data": {
"active": true,
"value": 40,
},
"id": 4,
"name": "element_4",
},
{
"data": {
"active": true,
"value": 50,
},
"id": 5,
"name": "element_5",
},
{
"data": {
"active": true,
"value": 60,
},
"id": 6,
"name": "element_6",
},
{
"data": {
"active": true,
"value": 70,
},
"id": 7,
"name": "element_7",
},
{
"data": {
"active": true,
"value": 80,
},
"id": 8,
"name": "element_8",
},
{
"data": {
"active": true,
"value": 90,
},
"id": 9,
"name": "element_9",
},
{
"data": {
"active": true,
"value": 100,
},
"id": 10,
"name": "element_10",
},
{
"truncated": true,
},
],
"large_array_jsonb": [
{
"data": {
"active": true,
"value": 10,
},
"id": 1,
"name": "element_1",
},
{
"data": {
"active": true,
"value": 20,
},
"id": 2,
"name": "element_2",
},
{
"data": {
"active": true,
"value": 30,
},
"id": 3,
"name": "element_3",
},
{
"data": {
"active": true,
"value": 40,
},
"id": 4,
"name": "element_4",
},
{
"data": {
"active": true,
"value": 50,
},
"id": 5,
"name": "element_5",
},
{
"data": {
"active": true,
"value": 60,
},
"id": 6,
"name": "element_6",
},
{
"data": {
"active": true,
"value": 70,
},
"id": 7,
"name": "element_7",
},
{
"data": {
"active": true,
"value": 80,
},
"id": 8,
"name": "element_8",
},
{
"data": {
"active": true,
"value": 90,
},
"id": 9,
"name": "element_9",
},
{
"data": {
"active": true,
"value": 100,
},
"id": 10,
"name": "element_10",
},
{
"truncated": true,
},
],
"name": "Large Array Item",
},
{
"id": 2,
"large_array_json": [
{
"id": 1,
"value": 1,
},
{
"id": 2,
"value": 2,
},
{
"id": 3,
"value": 3,
},
{
"id": 4,
"value": 4,
},
{
"id": 5,
"value": 5,
},
{
"id": 6,
"value": 6,
},
{
"id": 7,
"value": 7,
},
{
"id": 8,
"value": 8,
},
{
"id": 9,
"value": 9,
},
{
"id": 10,
"value": 10,
},
{
"truncated": true,
},
],
"large_array_jsonb": [
{
"id": 1,
"value": 1,
},
{
"id": 2,
"value": 2,
},
{
"id": 3,
"value": 3,
},
{
"id": 4,
"value": 4,
},
{
"id": 5,
"value": 5,
},
{
"id": 6,
"value": 6,
},
{
"id": 7,
"value": 7,
},
{
"id": 8,
"value": 8,
},
{
"id": 9,
"value": 9,
},
{
"id": 10,
"value": 10,
},
{
"truncated": true,
},
],
"name": "Large Array Small items",
},
{
"id": 3,
"large_array_json": [
{
"id": 1,
"tag": "tag1",
},
{
"id": 2,
"tag": "tag2",
},
{
"id": 3,
"tag": "tag3",
},
],
"large_array_jsonb": [
{
"id": 1,
"tag": "tag1",
},
{
"id": 2,
"tag": "tag2",
},
{
"id": 3,
"tag": "tag3",
},
],
"name": "Normal Array Item",
},
]
`)
}
)
withTestDatabase('should truncate fields to maxCharacters avoid', async (db) => {
// Create test table with array column
await db.executeQuery(`
CREATE TABLE test_large_array_table (
id SERIAL PRIMARY KEY,
name TEXT,
large_array TEXT[] -- Will hold a very large array
);
-- Insert test data with a large array (>10KB)
-- Create an array with 1000 elements to ensure it exceeds 10KB
INSERT INTO test_large_array_table (name, large_array) VALUES
('Normal Array Item', ARRAY['tag1', 'tag2', 'tag3']),
-- Locally testing with up to 700 Mo in size should work and not raise a JS string alloc size error
(repeat('A', 5 * 1024 * 1024), ARRAY['tag1', 'tag2', 'tag3']);
`)
// Get table metadata
const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
const tables = tablesZod.parse(await db.executeQuery(tablesSql))
const testTable = tables.find((table) => table.name === 'test_large_array_table')
expect(testTable).toBeDefined()
// Generate SQL with lower maxCharacters and maxArraySize limits
const sql = getTableRowsSql({
table: testTable!,
page: 1,
limit: 10,
maxCharacters: 256,
})
// Verify the SQL contains the array truncation logic
expect(sql).toMatchInlineSnapshot(`
"with _base_query as (select * from public.test_large_array_table order by test_large_array_table.id asc nulls last limit 10 offset 0)
select id,case
when octet_length(name::text) > 256
then left(name::text, 256) || '...'
else name::text
end as name,
case
when octet_length(large_array::text) > 256
then
case
when array_ndims(large_array) = 1
then
(select array_cat(large_array[1:50]::text[], array['...']::text[]))::text[]
else
large_array[1:50]::text[]
end
else large_array::text[]
end
from _base_query;"
`)
// Execute the SQL and verify results
const start = performance.now()
const queryResult = await db.executeQuery(sql)
const end = performance.now()
expect(end - start).lessThan(1000)
expect(queryResult).toMatchInlineSnapshot(`
[
{
"id": 1,
"large_array": [
"tag1",
"tag2",
"tag3",
],
"name": "Normal Array Item",
},
{
"id": 2,
"large_array": [
"tag1",
"tag2",
"tag3",
],
"name": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...",
},
]
`)
})
withTestDatabase('should generate SQL with filtering', async (db) => {
// Create test table and insert data
await db.executeQuery(`
CREATE TABLE test_sql_filter (
id SERIAL PRIMARY KEY,
name TEXT,
category TEXT
);
-- Insert test data with different categories
INSERT INTO test_sql_filter (name, category) VALUES
('Test Item 1', 'A'),
('Test Item 2', 'B'),
('Test Item 3', 'A'),
('Another Item', 'A'),
('Different Item', 'C');
`)
// Get table metadata
const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
const tables = tablesZod.parse(await db.executeQuery(tablesSql))
const testTable = tables.find((table) => table.name === 'test_sql_filter')
expect(testTable).toBeDefined()
// Define filters
const filters: Filter[] = [
{ column: 'name', operator: '~~', value: 'Test%' },
{ column: 'category', operator: '=', value: 'A' },
]
// Generate SQL with filters
const sql = getTableRowsSql({
table: testTable!,
filters,
page: 1,
limit: 10,
})
// Verify SQL generation with snapshot
expect(sql).toMatchInlineSnapshot(
`
"with _base_query as (select * from public.test_sql_filter where name::text ~~ 'Test%' and category = 'A' order by test_sql_filter.id asc nulls last limit 10 offset 0)
select id,case
when octet_length(name::text) > 10240
then left(name::text, 10240) || '...'
else name::text
end as name,case
when octet_length(category::text) > 10240
then left(category::text, 10240) || '...'
else category::text
end as category from _base_query;"
`
)
// E2E Test: Execute the SQL and verify results
const queryResult = await db.executeQuery(sql)
expect(queryResult.length).toBe(2) // Should only get items that match both filters
expect(queryResult.map((row: any) => row.name)).toEqual(['Test Item 1', 'Test Item 3'])
expect(queryResult.every((row: any) => row.category === 'A')).toBe(true)
})
withTestDatabase('should generate SQL with sorting', async (db) => {
// Create test table and insert data
await db.executeQuery(`
CREATE TABLE test_sql_sort (
id SERIAL PRIMARY KEY,
name TEXT,
value INTEGER
);
-- Insert test data with varying values
INSERT INTO test_sql_sort (name, value) VALUES
('Z Item', 10),
('A Item', 30),
('M Item', 20),
('X Item', null);
`)
// Get table metadata
const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
const tables = tablesZod.parse(await db.executeQuery(tablesSql))
const testTable = tables.find((table) => table.name === 'test_sql_sort')
expect(testTable).toBeDefined()
// Define sorts
const sorts: Sort[] = [
{ column: 'name', table: 'test_sql_sort', ascending: true, nullsFirst: false },
{ column: 'value', table: 'test_sql_sort', ascending: false, nullsFirst: true },
]
// Generate SQL with sorting
const sql = getTableRowsSql({
table: testTable!,
sorts,
page: 1,
limit: 10,
})
// Verify SQL generation with snapshot
expect(sql).toMatchInlineSnapshot(
`
"with _base_query as (select * from public.test_sql_sort order by test_sql_sort.name asc nulls last, test_sql_sort.value desc nulls first limit 10 offset 0)
select id,case
when octet_length(name::text) > 10240
then left(name::text, 10240) || '...'
else name::text
end as name,value from _base_query;"
`
)
// E2E Test: Execute the SQL and verify results
const queryResult = await db.executeQuery(sql)
expect(queryResult.length).toBe(4)
// Should be sorted by name (asc) first, then by value (desc, nulls first)
expect(queryResult.map((row: any) => row.name)).toEqual([
'A Item',
'M Item',
'X Item',
'Z Item',
])
// The first item (A Item) should have value 30
expect(queryResult[0].value).toBe(30)
// Check if the X Item (with null value) is before Z Item (non-null value)
// due to nullsFirst: true for the value sort
const xItemIndex = queryResult.findIndex((row: any) => row.name === 'X Item')
const zItemIndex = queryResult.findIndex((row: any) => row.name === 'Z Item')
expect(xItemIndex).toBeLessThan(zItemIndex)
})
withTestDatabase('should generate SQL for special/quoted column names', async (db) => {
// Create test table with quoted names and insert data
await db.executeQuery(`
CREATE TABLE "test sql spaces" (
id SERIAL PRIMARY KEY,
"user name" TEXT,
"column-with-dashes" TEXT,
"quoted""column" TEXT
);
-- Insert test data
INSERT INTO "test sql spaces" ("user name", "column-with-dashes", "quoted""column") VALUES
('User 1', 'Value 1', 'Quoted 1'),
('User 2', 'Value 2', 'Quoted 2');
`)
// Get table metadata
const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
const tables = tablesZod.parse(await db.executeQuery(tablesSql))
const testTable = tables.find((table) => table.name === 'test sql spaces')
expect(testTable).toBeDefined()
// Generate SQL
const sql = getTableRowsSql({
table: testTable!,
page: 1,
limit: 10,
})
// Verify SQL generation with snapshot
expect(sql).toMatchInlineSnapshot(
`
"with _base_query as (select * from public."test sql spaces" order by "test sql spaces".id asc nulls last limit 10 offset 0)
select id,case
when octet_length("user name"::text) > 10240
then left("user name"::text, 10240) || '...'
else "user name"::text
end as "user name",case
when octet_length("column-with-dashes"::text) > 10240
then left("column-with-dashes"::text, 10240) || '...'
else "column-with-dashes"::text
end as "column-with-dashes",case
when octet_length("quoted""column"::text) > 10240
then left("quoted""column"::text, 10240) || '...'
else "quoted""column"::text
end as "quoted""column" from _base_query;"
`
)
// E2E Test: Execute the SQL and verify results
const queryResult = await db.executeQuery(sql)
expect(queryResult.length).toBe(2)
expect(queryResult.map((row: any) => row['user name'])).toEqual(['User 1', 'User 2'])
expect(queryResult.map((row: any) => row['column-with-dashes'])).toEqual([
'Value 1',
'Value 2',
])
expect(queryResult.map((row: any) => row['quoted"column'])).toEqual(['Quoted 1', 'Quoted 2'])
})
withTestDatabase('should generate SQL for tables with large text fields', async (db) => {
// Create test table with large text fields
await db.executeQuery(`
CREATE TABLE test_large_text (
id SERIAL PRIMARY KEY,
small_text VARCHAR(100),
large_text TEXT,
json_data JSONB
);
-- Insert test data including a large text field
INSERT INTO test_large_text (small_text, large_text, json_data) VALUES
('Small text', repeat('Lorem ipsum ', 100), '{"key": "value", "nested": {"data": true}}'),
('Another small text', repeat('Dolor sit amet ', 100), '{"array": [1, 2, 3], "bool": false}');
`)
// Get table metadata
const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
const tables = tablesZod.parse(await db.executeQuery(tablesSql))
const testTable = tables.find((table) => table.name === 'test_large_text')
expect(testTable).toBeDefined()
// Generate SQL
const sql = getTableRowsSql({
table: testTable!,
page: 1,
limit: 10,
})
// Verify SQL generation with snapshot
expect(sql).toMatchInlineSnapshot(
`
"with _base_query as (select * from public.test_large_text order by test_large_text.id asc nulls last limit 10 offset 0)
select id,case
when octet_length(small_text::text) > 10240
then left(small_text::text, 10240) || '...'
else small_text::text
end as small_text,case
when octet_length(large_text::text) > 10240
then left(large_text::text, 10240) || '...'
else large_text::text
end as large_text,case
when octet_length(json_data::text) > 10240
then left(json_data::text, 10240) || '...'
else json_data::text
end as json_data from _base_query;"
`
)
// E2E Test: Execute the SQL and verify results
const queryResult = await db.executeQuery(sql)
expect(queryResult.length).toBe(2)
expect(queryResult.map((row: any) => row.small_text)).toEqual([
'Small text',
'Another small text',
])
expect(queryResult[0].large_text.startsWith('Lorem ipsum')).toBe(true)
expect(queryResult[1].large_text.startsWith('Dolor sit amet')).toBe(true)
expect(JSON.parse(queryResult[0].json_data)).toHaveProperty('key', 'value')
expect(JSON.parse(queryResult[1].json_data)).toHaveProperty('array')
})
withTestDatabase('should generate SQL with pagination', async (db) => {
// Create test table and insert multiple rows for pagination
await db.executeQuery(`
CREATE TABLE test_pagination (
id SERIAL PRIMARY KEY,
name TEXT
);
-- Insert 15 rows for pagination testing
INSERT INTO test_pagination (name)
SELECT 'Item ' || i FROM generate_series(1, 15) i;
`)
// Get table metadata
const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
const tables = tablesZod.parse(await db.executeQuery(tablesSql))
const testTable = tables.find((table) => table.name === 'test_pagination')
expect(testTable).toBeDefined()
// Generate SQL for page 1 (5 items)
const sql1 = getTableRowsSql({
table: testTable!,
page: 1,
limit: 5,
})
expect(sql1).toMatchInlineSnapshot(
`
"with _base_query as (select * from public.test_pagination order by test_pagination.id asc nulls last limit 5 offset 0)
select id,case
when octet_length(name::text) > 10240
then left(name::text, 10240) || '...'
else name::text
end as name from _base_query;"
`
)
const page1Result = await db.executeQuery(sql1)
expect(page1Result.length).toBe(5)
expect(page1Result.map((row: any) => row.name)).toEqual([
'Item 1',
'Item 2',
'Item 3',
'Item 4',
'Item 5',
])
const sql2 = getTableRowsSql({
table: testTable!,
page: 2,
limit: 5,
})
// Verify SQL generation for page 2
expect(sql2).toMatchInlineSnapshot(
`
"with _base_query as (select * from public.test_pagination order by test_pagination.id asc nulls last limit 5 offset 5)
select id,case
when octet_length(name::text) > 10240
then left(name::text, 10240) || '...'
else name::text
end as name from _base_query;"
`
)
const page2Result = await db.executeQuery(sql2)
expect(page2Result.length).toBe(5)
expect(page2Result.map((row: any) => row.name)).toEqual([
'Item 6',
'Item 7',
'Item 8',
'Item 9',
'Item 10',
])
})
withTestDatabase('should generate SQL for view', async (db) => {
// Create table and view
await db.executeQuery(`
CREATE TABLE test_view_source (
id SERIAL PRIMARY KEY,
name TEXT,
active BOOLEAN
);
-- Insert test data
INSERT INTO test_view_source (name, active) VALUES
('Active Item 1', true),
('Inactive Item', false),
('Active Item 2', true);
-- Create view that only shows active items
CREATE VIEW test_sql_view AS
SELECT id, name, active FROM test_view_source WHERE active = true;
`)
// Get view metadata
const { sql: viewsSql, zod: viewsZod } = pgMeta.views.list()
const views = viewsZod.parse(await db.executeQuery(viewsSql))
const testView = views.find((view) => view.name === 'test_sql_view')
expect(testView).toBeDefined()
// Generate SQL
const sql = getTableRowsSql({
table: testView!,
page: 1,
limit: 10,
})
// Verify SQL generation with snapshot
expect(sql).toMatchInlineSnapshot(
`
"with _base_query as (select * from public.test_sql_view order by test_sql_view.id asc nulls last limit 10 offset 0)
select id,case
when octet_length(name::text) > 10240
then left(name::text, 10240) || '...'
else name::text
end as name,active from _base_query;"
`
)
// E2E Test: Execute the SQL and verify results
const queryResult = await db.executeQuery(sql)
expect(queryResult.length).toBe(2) // Only active items should be in the view
expect(queryResult.map((row: any) => row.name)).toEqual(['Active Item 1', 'Active Item 2'])
expect(queryResult.every((row: any) => row.active === true)).toBe(true)
})
withTestDatabase('should generate SQL for materialized view', async (db) => {
// Create table and materialized view
await db.executeQuery(`
CREATE TABLE test_mv_source (
id SERIAL PRIMARY KEY,
name TEXT,
value NUMERIC
);
-- Insert test data
INSERT INTO test_mv_source (name, value) VALUES
('Item 1', 10.5),
('Item 2', -5.25),
('Item 3', 20);
-- Create materialized view that only includes positive values
CREATE MATERIALIZED VIEW test_sql_mv AS
SELECT id, name, value FROM test_mv_source WHERE value > 0;
`)
// Get materialized view metadata
const { sql: mvSql, zod: mvZod } = pgMeta.materializedViews.list()
const materializedViews = mvZod.parse(await db.executeQuery(mvSql))
const testMv = materializedViews.find((mv) => mv.name === 'test_sql_mv')
expect(testMv).toBeDefined()
// Generate SQL
const sql = getTableRowsSql({
table: testMv!,
page: 1,
limit: 10,
})
// Verify SQL generation with snapshot
expect(sql).toMatchInlineSnapshot(
`
"with _base_query as (select * from public.test_sql_mv order by test_sql_mv.id asc nulls last limit 10 offset 0)
select id,case
when octet_length(name::text) > 10240
then left(name::text, 10240) || '...'
else name::text
end as name,value from _base_query;"
`
)
// E2E Test: Execute the SQL and verify results
const queryResult = await db.executeQuery(sql)
expect(queryResult.length).toBe(2) // Only items with positive values
expect(queryResult.map((row: any) => row.name)).toEqual(['Item 1', 'Item 3'])
expect(queryResult.every((row: any) => row.value > 0)).toBe(true)
})
withTestDatabase('should generate SQL for foreign table', async (db) => {
// Set up a foreign table with the file_fdw extension
await db.executeQuery(`
-- Create the extension if it doesn't exist
CREATE EXTENSION IF NOT EXISTS file_fdw;
-- Create a foreign server
DROP SERVER IF EXISTS file_server2 CASCADE;
CREATE SERVER file_server2 FOREIGN DATA WRAPPER file_fdw;
-- Create a table to export data from
CREATE TABLE source_for_foreign_test (
id SERIAL PRIMARY KEY,
name TEXT,
description TEXT
);
-- Insert test data
INSERT INTO source_for_foreign_test (name, description) VALUES
('Foreign Item 1', 'Description 1'),
('Foreign Item 2', 'Description 2');
-- Export to CSV for the foreign table
COPY source_for_foreign_test TO '/tmp/foreign_test2.csv' WITH (FORMAT csv, HEADER);
-- Create the foreign table
CREATE FOREIGN TABLE test_sql_foreign (
id INT,
name TEXT,
description TEXT
) SERVER file_server2
OPTIONS (filename '/tmp/foreign_test2.csv', format 'csv', header 'true');
`)
// Get foreign table metadata
const { sql: ftSql, zod: ftZod } = pgMeta.foreignTables.list()
const foreignTables = ftZod.parse(await db.executeQuery(ftSql))
const testFt = foreignTables.find((ft) => ft.name === 'test_sql_foreign')
expect(testFt).toBeDefined()
// Generate SQL
const sql = getTableRowsSql({
table: testFt!,
page: 1,
limit: 10,
})
// Verify SQL generation with snapshot
expect(sql).toMatchInlineSnapshot(
`
"with _base_query as (select * from public.test_sql_foreign order by test_sql_foreign.id asc nulls last limit 10 offset 0)
select id,case
when octet_length(name::text) > 10240
then left(name::text, 10240) || '...'
else name::text
end as name,case
when octet_length(description::text) > 10240
then left(description::text, 10240) || '...'
else description::text
end as description from _base_query;"
`
)
// E2E Test: Execute the SQL and verify results
const queryResult = await db.executeQuery(sql)
expect(queryResult).toMatchInlineSnapshot(`
[
{
"description": "Description 1",
"id": 1,
"name": "Foreign Item 1",
},
{
"description": "Description 2",
"id": 2,
"name": "Foreign Item 2",
},
]
`)
})
})
withTestDatabase('should handle large multi-dimensional arrays correctly', async (db) => {
// Create test table with multi-dimensional arrays
await db.executeQuery(`
CREATE TABLE public.monitor_data (
subject_id TEXT,
"timestamp" TIMESTAMP[],
"PPG" FLOAT8[][],
"ACC" FLOAT8[][]
);
INSERT INTO public.monitor_data (subject_id, "timestamp", "PPG", "ACC")
VALUES (
'subject-1',
ARRAY['2024-01-01 00:00:00'::timestamp, '2024-01-02 00:00:00'::timestamp],
ARRAY[
[1.1, 1.2, 1.3, 1.4, 1.5, 1.6],
[2.1, 2.2, 2.3, 2.4, 2.5, 2.6],
[3.1, 3.2, 3.3, 3.4, 3.5, 3.6]
]::FLOAT8[][],
ARRAY[
[4.1, 4.2, 4.3, 4.4, 4.5, 4.6],
[5.1, 5.2, 5.3, 5.4, 5.5, 5.6],
[6.1, 6.2, 6.3, 6.4, 6.5, 6.6]
]::FLOAT8[][]
);
INSERT INTO public.monitor_data (subject_id, "timestamp", "PPG", "ACC")
VALUES (
'subject-large',
-- large 1D timestamp array (e.g., 1000 timestamps)
ARRAY(
SELECT generate_series('2024-01-01'::timestamp, '2024-01-01'::timestamp + interval '999 minutes', '1 minute')
),
-- large 2D float8 arrays (e.g., 1000 x 6)
ARRAY(
SELECT ARRAY[
random(), random(), random(), random(), random(), random()
]::float8[]
FROM generate_series(1, 1000)
)::float8[][],
ARRAY(
SELECT ARRAY[
random(), random(), random(), random(), random(), random()
]::float8[]
FROM generate_series(1, 1000)
)::float8[][]
);
`)
// Get table metadata
const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
const tables = tablesZod.parse(await db.executeQuery(tablesSql))
const testTable = tables.find((table) => table.name === 'monitor_data')
expect(testTable).toBeDefined()
// Generate SQL with default settings
const sql = getTableRowsSql({
table: testTable!,
page: 1,
limit: 10,
})
// Verify SQL generation with snapshot
expect(sql).toMatchInlineSnapshot(`
"with _base_query as (select * from public.monitor_data order by monitor_data.subject_id asc nulls last limit 10 offset 0)
select case
when octet_length(subject_id::text) > 10240
then left(subject_id::text, 10240) || '...'
else subject_id::text
end as subject_id,
case
when octet_length("timestamp"::text) > 10240
then
case
when array_ndims("timestamp") = 1
then
(select array_cat("timestamp"[1:50]::text[], array['...']::text[]))::text[]
else
"timestamp"[1:50]::text[]
end
else "timestamp"::text[]
end
,
case
when octet_length("PPG"::text) > 10240
then
case
when array_ndims("PPG") = 1
then
(select array_cat("PPG"[1:50]::text[], array['...']::text[]))::text[]
else
"PPG"[1:50]::text[]
end
else "PPG"::text[]
end
,
case
when octet_length("ACC"::text) > 10240
then
case
when array_ndims("ACC") = 1
then
(select array_cat("ACC"[1:50]::text[], array['...']::text[]))::text[]
else
"ACC"[1:50]::text[]
end
else "ACC"::text[]
end
from _base_query;"
`)
// Execute the SQL and verify results
const queryResult = await db.executeQuery(sql)
expect(queryResult.length).toBe(2)
// Verify the first row (small arrays)
const smallRow = queryResult.find((row: any) => row.subject_id === 'subject-1')
expect(smallRow).toBeDefined()
expect(smallRow.timestamp).toHaveLength(2)
expect(smallRow.PPG).toHaveLength(3)
expect(smallRow.PPG[0]).toHaveLength(6)
expect(smallRow.ACC).toHaveLength(3)
expect(smallRow.ACC[0]).toHaveLength(6)
// Verify the second row (large arrays)
const largeRow = queryResult.find((row: any) => row.subject_id === 'subject-large')
expect(largeRow).toBeDefined()
expect(largeRow.timestamp).toHaveLength(51) // Has the extra '...' element
expect(largeRow.PPG).toHaveLength(50)
expect(largeRow.PPG[0]).toHaveLength(6)
expect(largeRow.ACC).toHaveLength(50)
expect(largeRow.ACC[0]).toHaveLength(6)
// Test with custom maxArraySize
const sqlWithCustomSize = getTableRowsSql({
table: testTable!,
page: 1,
limit: 10,
maxArraySize: 10,
})
const customSizeResult = await db.executeQuery(sqlWithCustomSize)
const largeRowCustom = customSizeResult.find((row: any) => row.subject_id === 'subject-large')
expect(largeRowCustom.timestamp).toHaveLength(11) // Has the extra '...' element
expect(largeRowCustom.PPG).toHaveLength(10) // multi-dimentional array are truncated
expect(largeRowCustom.ACC).toHaveLength(10)
})
withTestDatabase('should handle reserved keyword "collation" as column name', async (db) => {
// Create a table with a column named "collation" (a PostgreSQL reserved keyword)
await db.executeQuery(`
CREATE TABLE IF NOT EXISTS "public"."test" (
id SERIAL PRIMARY KEY,
"collation" TEXT
);
DELETE FROM "public"."test";
INSERT INTO "public"."test" ("collation")
VALUES
('value1'),
('value2'),
('value3');
`)
// Get table metadata
const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
const tables = tablesZod.parse(await db.executeQuery(tablesSql))
const testTable = tables.find((table) => table.name === 'test')
expect(testTable).toBeDefined()
// Generate SQL - this should properly quote the "collation" column name
const sql = getTableRowsSql({
table: testTable!,
page: 1,
limit: 100,
})
// Verify SQL generation - the "collation" column should be properly quoted
expect(sql).toMatchInlineSnapshot(`
"with _base_query as (select * from public.test order by test.id asc nulls last limit 100 offset 0)
select id,case
when octet_length("collation"::text) > 10240
then left("collation"::text, 10240) || '...'
else "collation"::text
end as "collation" from _base_query;"
`)
// Execute the generated SQL and verify the results
const queryResult = await db.executeQuery(sql)
expect(queryResult.length).toBe(3)
expect(queryResult[0].collation).toBe('value1')
expect(queryResult[1].collation).toBe('value2')
expect(queryResult[2].collation).toBe('value3')
// Test with ORDER BY on the collation column
const sqlWithOrder = getTableRowsSql({
table: testTable!,
page: 1,
limit: 100,
sorts: [{ table: 'test', column: 'collation', ascending: true, nullsFirst: false }],
})
// Verify the ORDER BY clause properly quotes the collation column
expect(sqlWithOrder).toMatchInlineSnapshot(`
"with _base_query as (select * from public.test order by test."collation" asc nulls last limit 100 offset 0)
select id,case
when octet_length("collation"::text) > 10240
then left("collation"::text, 10240) || '...'
else "collation"::text
end as "collation" from _base_query;"
`)
const queryResultWithOrder = await db.executeQuery(sqlWithOrder)
expect(queryResultWithOrder.length).toBe(3)
expect(queryResultWithOrder[0].collation).toBe('value1')
expect(queryResultWithOrder[1].collation).toBe('value2')
expect(queryResultWithOrder[2].collation).toBe('value3')
// Test with FILTER on the collation column
const sqlWithFilter = getTableRowsSql({
table: testTable!,
page: 1,
limit: 100,
filters: [{ column: 'collation', operator: '=', value: 'value2' }],
})
// Verify the WHERE clause properly quotes the collation column
expect(sqlWithFilter).toMatchInlineSnapshot(`
"with _base_query as (select * from public.test where "collation" = 'value2' order by test.id asc nulls last limit 100 offset 0)
select id,case
when octet_length("collation"::text) > 10240
then left("collation"::text, 10240) || '...'
else "collation"::text
end as "collation" from _base_query;"
`)
const queryResultWithFilter = await db.executeQuery(sqlWithFilter)
expect(queryResultWithFilter.length).toBe(1)
expect(queryResultWithFilter[0].collation).toBe('value2')
})
})