{"version":3,"file":"pool.test.d.ts","sourceRoot":"","sources":["../src/pool.test.ts"],"names":[],"mappings":"","sourcesContent":["import { Writable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\n\nimport { afterAll, assert, beforeAll, describe, expect, it } from 'vitest';\nimport { ZodError, z } from 'zod';\n\nimport {\n  callOptionalRow,\n  callOptionalScalar,\n  callRow,\n  callRows,\n  callScalar,\n  callScalars,\n  execute,\n  queryCursor,\n  queryOptionalRow,\n  queryOptionalScalar,\n  queryRow,\n  queryRows,\n  queryScalar,\n  queryScalars,\n} from './default-pool.js';\nimport { makePostgresTestUtils } from './test-utils.js';\n\nconst postgresTestUtils = makePostgresTestUtils({\n  database: 'prairielearn_postgres',\n});\n\nconst WorkspaceSchema = z.object({\n  id: z.string(),\n  created_at: z.date(),\n});\n\nconst SprocTwoColumnsSchema = z.object({\n  id: z.string(),\n  negative: z.number(),\n});\n\ndescribe('@prairielearn/postgres', function () {\n  beforeAll(async () => {\n    await postgresTestUtils.createDatabase();\n    await execute(\n      'CREATE TABLE workspaces (id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP);',\n    );\n    await execute('INSERT INTO workspaces (id) SELECT s FROM generate_series(1, 100) AS s');\n    await execute(\n      'CREATE FUNCTION test_sproc_one_column(num_entries INT) RETURNS TABLE (id BIGINT) AS $$ BEGIN RETURN QUERY SELECT s::BIGINT AS id FROM generate_series(1, num_entries) AS s; END; $$ LANGUAGE plpgsql;',\n    );\n    await execute(\n      'CREATE FUNCTION test_sproc_two_columns(num_entries INT) RETURNS TABLE (id BIGINT, negative INT) AS $$ BEGIN RETURN QUERY SELECT s::BIGINT AS id, -s AS negative FROM generate_series(1, num_entries) AS s; END; $$ LANGUAGE plpgsql;',\n    );\n    await execute(\n      'CREATE FUNCTION test_sproc_one_column_ten_rows() RETURNS TABLE (id BIGINT) AS $$ BEGIN RETURN QUERY SELECT s::BIGINT AS id FROM generate_series(1, 10) AS s; END; $$ LANGUAGE plpgsql;',\n    );\n    await execute(\n      'CREATE FUNCTION test_sproc_one_column_one_row(OUT id BIGINT) AS $$ BEGIN id = 1; END; $$ LANGUAGE plpgsql;',\n    );\n  });\n\n  afterAll(async () => {\n    await postgresTestUtils.dropDatabase();\n  });\n\n  describe('paramsToArray', () => {\n    it('enforces SQL must be a string', async () => {\n      // @ts-expect-error SQL must be a string\n      const rows = execute({ invalid: true }, {});\n      await expect(rows).rejects.toThrow('SQL must be a string');\n    });\n\n    it('enforces params must be array or object', async () => {\n      // @ts-expect-error params must be an array or object\n      const rows = execute('SELECT 33;', 33);\n      await expect(rows).rejects.toThrow('params must be array or object');\n    });\n\n    it('rejects missing parameters', async () => {\n      const rows = execute('SELECT $missing;', {});\n      await expect(rows).rejects.toThrow('Missing parameter');\n    });\n\n    it('rejects unused parameters in testing', async () => {\n      const rows = execute('SELECT 33;', { unsed_parameter: true });\n      await expect(rows).rejects.toThrow('Unused parameter');\n    });\n  });\n\n  describe('queryRows', () => {\n    it('handles single column', async () => {\n      const rows = await queryRows(\n        'SELECT id FROM workspaces WHERE id <= 10;',\n        z.object({ id: z.string() }),\n      );\n      assert.lengthOf(rows, 10);\n      assert.equal(rows[0].id, '1');\n    });\n\n    it('handles multiple columns', async () => {\n      const rows = await queryRows('SELECT * FROM workspaces WHERE id <= 10;', WorkspaceSchema);\n      assert.lengthOf(rows, 10);\n      assert.equal(rows[0].id, '1');\n      assert.isNotNull(rows[0].created_at);\n    });\n\n    it('handles parameters', async () => {\n      const rows = await queryRows(\n        'SELECT * FROM workspaces WHERE id <= $1;',\n        [10],\n        WorkspaceSchema,\n      );\n      assert.lengthOf(rows, 10);\n    });\n  });\n\n  describe('queryRow', () => {\n    it('handles single column', async () => {\n      const row = await queryRow(\n        'SELECT id FROM workspaces WHERE id = 1;',\n        z.object({ id: z.string() }),\n      );\n      assert.equal(row.id, '1');\n    });\n\n    it('handles multiple columns', async () => {\n      const row = await queryRow('SELECT * FROM workspaces WHERE id = 1;', WorkspaceSchema);\n      assert.equal(row.id, '1');\n      assert.isNotNull(row.created_at);\n    });\n\n    it('handles parameters', async () => {\n      const row = await queryRow('SELECT * FROM workspaces WHERE id = $1;', [1], WorkspaceSchema);\n      assert.equal(row.id, '1');\n    });\n\n    it('rejects results with zero rows', async () => {\n      const rows = queryRow('SELECT * FROM workspaces WHERE id = -1;', WorkspaceSchema);\n      await expect(rows).rejects.toThrow('Incorrect rowCount: 0');\n    });\n\n    it('rejects results with multiple rows', async () => {\n      const rows = queryRow('SELECT * FROM workspaces', WorkspaceSchema);\n      await expect(rows).rejects.toThrow('Incorrect rowCount: 100');\n    });\n  });\n\n  describe('queryOptionalRow', () => {\n    it('handles single column', async () => {\n      const row = await queryRow(\n        'SELECT id FROM workspaces WHERE id = 1;',\n        z.object({ id: z.string() }),\n      );\n      assert.equal(row.id, '1');\n    });\n\n    it('handles multiple columns', async () => {\n      const row = await queryOptionalRow('SELECT * FROM workspaces WHERE id = 1;', WorkspaceSchema);\n      assert.isNotNull(row);\n      assert.equal(row?.id, '1');\n      assert.isNotNull(row?.created_at);\n    });\n\n    it('handles parameters', async () => {\n      const row = await queryOptionalRow(\n        'SELECT * FROM workspaces WHERE id = $1;',\n        [1],\n        WorkspaceSchema,\n      );\n      assert.isNotNull(row);\n      assert.equal(row?.id, '1');\n    });\n\n    it('handles missing result', async () => {\n      const row = await queryOptionalRow(\n        'SELECT * FROM workspaces WHERE id = -1;',\n        WorkspaceSchema,\n      );\n      assert.isNull(row);\n    });\n\n    it('rejects with multiple rows', async () => {\n      const rows = queryOptionalRow('SELECT * FROM workspaces', WorkspaceSchema);\n      await expect(rows).rejects.toThrow('Incorrect rowCount: 100');\n    });\n  });\n\n  describe('callRows', () => {\n    it('handles single column', async () => {\n      const rows = await callRows('test_sproc_one_column_ten_rows', z.object({ id: z.string() }));\n      assert.lengthOf(rows, 10);\n      assert.equal(rows[0].id, '1');\n    });\n\n    it('handles parameters', async () => {\n      const rows = await callRows('test_sproc_one_column', [10], z.object({ id: z.string() }));\n      assert.lengthOf(rows, 10);\n      assert.equal(rows[0].id, '1');\n    });\n\n    it('handles multiple columns', async () => {\n      const rows = await callRows('test_sproc_two_columns', [20], SprocTwoColumnsSchema);\n      assert.lengthOf(rows, 20);\n      assert.equal(rows[0].id, '1');\n      assert.equal(rows[0].negative, -1);\n      assert.equal(rows[19].id, '20');\n      assert.equal(rows[19].negative, -20);\n    });\n  });\n\n  describe('callRow', () => {\n    it('handles single column', async () => {\n      const row = await callRow('test_sproc_one_column_one_row', z.object({ id: z.string() }));\n      assert.equal(row.id, '1');\n    });\n\n    it('handles parameters', async () => {\n      const row = await callRow('test_sproc_one_column', [1], z.object({ id: z.string() }));\n      assert.equal(row.id, '1');\n    });\n\n    it('handles multiple columns', async () => {\n      const row = await callRow('test_sproc_two_columns', [1], SprocTwoColumnsSchema);\n      assert.equal(row.id, '1');\n      assert.equal(row.negative, -1);\n    });\n\n    it('rejects results with zero rows', async () => {\n      const row = callRow('test_sproc_two_columns', [0], SprocTwoColumnsSchema);\n      await expect(row).rejects.toThrow('Incorrect rowCount: 0');\n    });\n\n    it('rejects results with multiple rows', async () => {\n      const rows = callRow('test_sproc_two_columns', [100], SprocTwoColumnsSchema);\n      await expect(rows).rejects.toThrow('Incorrect rowCount: 100');\n    });\n  });\n\n  describe('callOptionalRow', () => {\n    it('handles single column', async () => {\n      const row = await callOptionalRow(\n        'test_sproc_one_column_one_row',\n        z.object({ id: z.string() }),\n      );\n      assert.equal(row?.id, '1');\n    });\n\n    it('handles parameters', async () => {\n      const row = await callOptionalRow('test_sproc_one_column', [1], z.object({ id: z.string() }));\n      assert.equal(row?.id, '1');\n    });\n\n    it('handles multiple columns', async () => {\n      const row = await callOptionalRow('test_sproc_two_columns', [1], SprocTwoColumnsSchema);\n      assert.isNotNull(row);\n      assert.equal(row?.id, '1');\n      assert.equal(row?.negative, -1);\n    });\n\n    it('handles results with zero rows', async () => {\n      const row = await callOptionalRow('test_sproc_two_columns', [0], SprocTwoColumnsSchema);\n      assert.isNull(row);\n    });\n\n    it('rejects results with multiple rows', async () => {\n      const rows = callOptionalRow('test_sproc_two_columns', [100], SprocTwoColumnsSchema);\n      await expect(rows).rejects.toThrow('Incorrect rowCount: 100');\n    });\n  });\n\n  describe('queryScalars', () => {\n    it('returns all scalar values', async () => {\n      const ids = await queryScalars(\n        'SELECT id FROM workspaces WHERE id <= 10 ORDER BY id ASC;',\n        z.string(),\n      );\n      assert.lengthOf(ids, 10);\n      assert.equal(ids[0], '1');\n      assert.equal(ids[9], '10');\n    });\n\n    it('handles parameters', async () => {\n      const ids = await queryScalars(\n        'SELECT id FROM workspaces WHERE id <= $1 ORDER BY id ASC;',\n        [5],\n        z.string(),\n      );\n      assert.lengthOf(ids, 5);\n    });\n\n    it('rejects multi-column queries', async () => {\n      const result = queryScalars('SELECT * FROM workspaces WHERE id <= 10;', z.string());\n      await expect(result).rejects.toThrow('Expected exactly one column');\n    });\n  });\n\n  describe('queryScalar', () => {\n    it('returns a single scalar value', async () => {\n      const id = await queryScalar('SELECT id FROM workspaces WHERE id = 1;', z.string());\n      assert.equal(id, '1');\n    });\n\n    it('handles parameters', async () => {\n      const id = await queryScalar('SELECT id FROM workspaces WHERE id = $1;', [1], z.string());\n      assert.equal(id, '1');\n    });\n\n    it('rejects results with zero rows', async () => {\n      const result = queryScalar('SELECT id FROM workspaces WHERE id = -1;', z.string());\n      await expect(result).rejects.toThrow('Incorrect rowCount: 0');\n    });\n\n    it('rejects results with multiple rows', async () => {\n      const result = queryScalar('SELECT id FROM workspaces;', z.string());\n      await expect(result).rejects.toThrow('Incorrect rowCount: 100');\n    });\n\n    it('rejects multi-column queries', async () => {\n      const result = queryScalar('SELECT * FROM workspaces WHERE id = 1;', z.string());\n      await expect(result).rejects.toThrow('Expected exactly one column');\n    });\n  });\n\n  describe('queryOptionalScalar', () => {\n    it('returns a scalar value when present', async () => {\n      const id = await queryOptionalScalar('SELECT id FROM workspaces WHERE id = 1;', z.string());\n      assert.equal(id, '1');\n    });\n\n    it('handles parameters', async () => {\n      const id = await queryOptionalScalar(\n        'SELECT id FROM workspaces WHERE id = $1;',\n        [1],\n        z.string(),\n      );\n      assert.equal(id, '1');\n    });\n\n    it('returns null for zero rows', async () => {\n      const id = await queryOptionalScalar('SELECT id FROM workspaces WHERE id = -1;', z.string());\n      assert.isNull(id);\n    });\n\n    it('rejects results with multiple rows', async () => {\n      const result = queryOptionalScalar('SELECT id FROM workspaces;', z.string());\n      await expect(result).rejects.toThrow('Incorrect rowCount: 100');\n    });\n\n    it('rejects multi-column queries', async () => {\n      const result = queryOptionalScalar('SELECT * FROM workspaces WHERE id = 1;', z.string());\n      await expect(result).rejects.toThrow('Expected exactly one column');\n    });\n  });\n\n  describe('callScalars', () => {\n    it('returns all scalar values', async () => {\n      const ids = await callScalars('test_sproc_one_column', [10], z.string());\n      assert.lengthOf(ids, 10);\n      assert.equal(ids[0], '1');\n    });\n\n    it('handles no parameters', async () => {\n      const ids = await callScalars('test_sproc_one_column_ten_rows', z.string());\n      assert.lengthOf(ids, 10);\n    });\n\n    it('rejects multi-column sprocs', async () => {\n      const result = callScalars('test_sproc_two_columns', [10], z.string());\n      await expect(result).rejects.toThrow('Expected exactly one column');\n    });\n  });\n\n  describe('callScalar', () => {\n    it('returns a single scalar value', async () => {\n      const id = await callScalar('test_sproc_one_column_one_row', z.string());\n      assert.equal(id, '1');\n    });\n\n    it('handles parameters', async () => {\n      const id = await callScalar('test_sproc_one_column', [1], z.string());\n      assert.equal(id, '1');\n    });\n\n    it('rejects results with zero rows', async () => {\n      const result = callScalar('test_sproc_one_column', [0], z.string());\n      await expect(result).rejects.toThrow('Incorrect rowCount: 0');\n    });\n\n    it('rejects results with multiple rows', async () => {\n      const result = callScalar('test_sproc_one_column', [100], z.string());\n      await expect(result).rejects.toThrow('Incorrect rowCount: 100');\n    });\n\n    it('rejects multi-column sprocs', async () => {\n      const result = callScalar('test_sproc_two_columns', [1], z.string());\n      await expect(result).rejects.toThrow('Expected exactly one column');\n    });\n  });\n\n  describe('callOptionalScalar', () => {\n    it('returns a scalar value when present', async () => {\n      const id = await callOptionalScalar('test_sproc_one_column_one_row', z.string());\n      assert.equal(id, '1');\n    });\n\n    it('handles parameters', async () => {\n      const id = await callOptionalScalar('test_sproc_one_column', [1], z.string());\n      assert.equal(id, '1');\n    });\n\n    it('returns null for zero rows', async () => {\n      const id = await callOptionalScalar('test_sproc_one_column', [0], z.string());\n      assert.isNull(id);\n    });\n\n    it('rejects results with multiple rows', async () => {\n      const result = callOptionalScalar('test_sproc_one_column', [100], z.string());\n      await expect(result).rejects.toThrow('Incorrect rowCount: 100');\n    });\n\n    it('rejects multi-column sprocs', async () => {\n      const result = callOptionalScalar('test_sproc_two_columns', [1], z.string());\n      await expect(result).rejects.toThrow('Expected exactly one column');\n    });\n  });\n\n  describe('queryCursor', () => {\n    it('handles single column', async () => {\n      const cursor = await queryCursor(\n        'SELECT id FROM workspaces WHERE id = 1;',\n        z.object({ id: z.string() }),\n      );\n      const allRows: { id: string }[] = [];\n      for await (const rows of cursor.iterate(10)) {\n        allRows.push(...rows);\n      }\n      assert.equal(allRows[0].id, '1');\n    });\n\n    it('handles multiple columns', async () => {\n      const cursor = await queryCursor('SELECT * FROM workspaces WHERE id = 1;', WorkspaceSchema);\n      const allRows: z.infer<typeof WorkspaceSchema>[] = [];\n      for await (const rows of cursor.iterate(10)) {\n        allRows.push(...rows);\n      }\n      assert.equal(allRows[0].id, '1');\n      assert.isNotNull(allRows[0].created_at);\n    });\n    it('returns zero rows', async () => {\n      const cursor = await queryCursor('SELECT * FROM workspaces WHERE id = 10000;', z.unknown());\n      const rowBatches = [];\n      for await (const rows of cursor.iterate(10)) {\n        rowBatches.push(rows);\n      }\n      assert.lengthOf(rowBatches, 0);\n    });\n\n    it('returns one row at a time', async () => {\n      const cursor = await queryCursor('SELECT * FROM workspaces WHERE id <= 2;', z.unknown());\n      const rowBatches = [];\n      for await (const rows of cursor.iterate(1)) {\n        rowBatches.push(rows);\n      }\n      assert.lengthOf(rowBatches, 2);\n      assert.lengthOf(rowBatches[0], 1);\n      assert.lengthOf(rowBatches[1], 1);\n    });\n\n    it('returns all rows at once', async () => {\n      const cursor = queryCursor('SELECT * FROM workspaces WHERE id <= 10;', z.unknown());\n      const rowBatches = [];\n      for await (const rows of (await cursor).iterate(10)) {\n        rowBatches.push(rows);\n      }\n      assert.lengthOf(rowBatches, 1);\n      assert.lengthOf(rowBatches[0], 10);\n    });\n\n    it('handles errors', async () => {\n      const cursor = await queryCursor('NOT VALID SQL', z.unknown());\n\n      async function readAllRows() {\n        const allRows = [];\n        for await (const rows of cursor.iterate(10)) {\n          allRows.push(...rows);\n        }\n        return allRows;\n      }\n\n      const maybeError = await readAllRows().catch((err) => err);\n      assert.instanceOf(maybeError, Error);\n      assert.match(maybeError.message, /syntax error/);\n      assert.isDefined((maybeError as any).data);\n      assert.equal((maybeError as any).data.sql, 'NOT VALID SQL');\n      assert.deepEqual((maybeError as any).data.sqlParams, {});\n      assert.isDefined((maybeError as any).data.sqlError);\n      assert.equal((maybeError as any).data.sqlError.severity, 'ERROR');\n    });\n  });\n\n  describe('queryCursor', () => {\n    const WorkspaceSchema = z.object({\n      id: z.string(),\n    });\n\n    const BadWorkspaceSchema = z.object({\n      badProperty: z.string(),\n    });\n\n    describe('iterator', () => {\n      it('validates with provided schema', async () => {\n        const cursor = await queryCursor(\n          'SELECT * FROM workspaces WHERE id <= 10 ORDER BY id ASC;',\n          WorkspaceSchema,\n        );\n        const allRows = [];\n        for await (const rows of cursor.iterate(10)) {\n          allRows.push(...rows);\n        }\n        assert.lengthOf(allRows, 10);\n        const workspace = allRows[0] as any;\n        assert.equal(workspace.id, '1');\n        assert.isUndefined(workspace.state);\n      });\n\n      it('throws error when validation fails', async () => {\n        const cursor = await queryCursor(\n          'SELECT * FROM workspaces WHERE id <= 10 ORDER BY id ASC;',\n          BadWorkspaceSchema,\n        );\n\n        async function readAllRows() {\n          const allRows = [];\n          for await (const rows of cursor.iterate(10)) {\n            allRows.push(...rows);\n          }\n          return allRows;\n        }\n\n        const maybeError = await readAllRows().catch((err) => err);\n        assert.instanceOf(maybeError, ZodError);\n        assert.lengthOf(maybeError.issues, 10);\n      });\n    });\n\n    describe('stream', () => {\n      it('validates with provided schema', async () => {\n        const cursor = await queryCursor(\n          'SELECT * FROM workspaces WHERE id <= 10 ORDER BY id ASC;',\n          WorkspaceSchema,\n        );\n        const stream = cursor.stream(1);\n        const allRows = [];\n        for await (const row of stream) {\n          allRows.push(row);\n        }\n\n        assert.lengthOf(allRows, 10);\n      });\n\n      it('emits an error when validation fails', async () => {\n        const cursor = await queryCursor(\n          'SELECT * FROM workspaces ORDER BY id ASC;',\n          BadWorkspaceSchema,\n        );\n        const stream = cursor.stream(1);\n\n        async function readAllRows() {\n          const allRows = [];\n          for await (const row of stream) {\n            allRows.push(row);\n          }\n          return allRows;\n        }\n\n        const maybeError = await readAllRows().catch((err) => err);\n        assert.instanceOf(maybeError, ZodError);\n        assert.lengthOf(maybeError.issues, 1);\n      });\n\n      it('closes the cursor when the stream is closed', async () => {\n        const cursor = await queryCursor('SELECT * FROM workspaces;', WorkspaceSchema);\n        const stream = cursor.stream(1);\n\n        const rows: any[] = [];\n        const ac = new AbortController();\n        const writable = new Writable({\n          objectMode: true,\n          write(chunk, _encoding, callback) {\n            rows.push(chunk);\n\n            // After receiving the first row, abort the stream. This lets us test\n            // that the underlying cursor is closed. If it is *not* closed, this\n            // `after` hook will fail with a timeout.\n            ac.abort();\n            callback();\n          },\n        });\n\n        await expect(pipeline(stream, writable, { signal: ac.signal })).rejects.toThrow();\n        assert.lengthOf(rows, 1);\n      });\n    });\n  });\n});\n"]}