Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix problems with local libSQL DB #12089

Merged
merged 12 commits into from
Oct 7, 2024
5 changes: 5 additions & 0 deletions .changeset/mighty-lions-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/db': patch
---

Fixes initial schema push for local file and in-memory libSQL DB
5 changes: 5 additions & 0 deletions .changeset/shy-knives-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/db': patch
---

Fixes relative local libSQL db URL
14 changes: 12 additions & 2 deletions packages/db/src/core/cli/migration-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
TextColumn,
} from '../types.js';
import type { RemoteDatabaseInfo, Result } from '../utils.js';
import { LibsqlError } from '@libsql/client';

const sqlite = new SQLiteAsyncDialect();
const genTempTableName = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10);
Expand Down Expand Up @@ -450,10 +451,19 @@ async function getDbCurrentSnapshot(
);

return JSON.parse(res.snapshot);
} catch (error: any) {
if (error.code === 'SQLITE_UNKNOWN') {
} catch (error) {
// Don't handle errors that are not from libSQL
if (error instanceof LibsqlError &&
// If the schema was never pushed to the database yet the table won't exist.
// Treat a missing snapshot table as an empty table.
(
// When connecting to a remote database in that condition
// the query will fail with the following error code and message.
(error.code === 'SQLITE_UNKNOWN' && error.message === 'SQLITE_UNKNOWN: SQLite error: no such table: _astro_db_snapshot') ||
// When connecting to a local or in-memory database that does not have a snapshot table yet
// the query will fail with the following error code and message.
(error.code === 'SQLITE_ERROR' && error.message === 'SQLITE_ERROR: no such table: _astro_db_snapshot'))
) {
return;
}

Expand Down
12 changes: 8 additions & 4 deletions packages/db/src/core/integration/vite-plugin-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,14 @@ export function getStudioVirtualModContents({
function dbUrlArg() {
const dbStr = JSON.stringify(dbInfo.url);

// Allow overriding, mostly for testing
return dbInfo.type === 'studio'
? `import.meta.env.ASTRO_STUDIO_REMOTE_DB_URL ?? ${dbStr}`
: `import.meta.env.ASTRO_DB_REMOTE_URL ?? ${dbStr}`;
if (isBuild) {
// Allow overriding, mostly for testing
return dbInfo.type === 'studio'
? `import.meta.env.ASTRO_STUDIO_REMOTE_DB_URL ?? ${dbStr}`
: `import.meta.env.ASTRO_DB_REMOTE_URL ?? ${dbStr}`;
} else {
return dbStr;
}
}

return `
Expand Down
27 changes: 24 additions & 3 deletions packages/db/src/runtime/db-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,38 @@ export function createRemoteDatabaseClient(options: RemoteDbClientOptions) {

return options.dbType === 'studio'
? createStudioDatabaseClient(options.appToken, remoteUrl)
: createRemoteLibSQLClient(options.appToken, remoteUrl);
: createRemoteLibSQLClient(options.appToken, remoteUrl, options.remoteUrl.toString());
}

function createRemoteLibSQLClient(appToken: string, remoteDbURL: URL) {
function createRemoteLibSQLClient(appToken: string, remoteDbURL: URL, rawUrl: string) {
const options: Partial<LibSQLConfig> = Object.fromEntries(remoteDbURL.searchParams.entries());
remoteDbURL.search = '';

let url = remoteDbURL.toString();
if (remoteDbURL.protocol === 'memory:') {
// libSQL expects a special string in place of a URL
// for in-memory DBs.
url = ':memory:';
} else if (
remoteDbURL.protocol === 'file:' &&
remoteDbURL.pathname.startsWith('/') &&
!rawUrl.startsWith('file:/')
) {
// libSQL accepts relative and absolute file URLs
ematipico marked this conversation as resolved.
Show resolved Hide resolved
// for local DBs. This doesn't match the URL specification.
// Parsing `file:some.db` and `file:/some.db` should yield
// the same result, but libSQL interprets the former as
// a relative path, and the latter as an absolute path.
// This detects when such a conversion happened during parsing
// and undoes it so that the URL given to libSQL is the
// same as given by the user.
url = 'file:' + remoteDbURL.pathname.substring(1);
}

const client = createClient({
...options,
authToken: appToken,
url: remoteDbURL.protocol === 'memory:' ? ':memory:' : remoteDbURL.toString(),
url,
});
return drizzleLibsql(client);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/db/test/basics.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('astro:db', () => {
});
});

describe('development', () => {
describe({ skip: process.platform === 'darwin' }, 'development', () => {
let devServer;

before(async () => {
Expand Down Expand Up @@ -94,7 +94,7 @@ describe('astro:db', () => {
});
});

describe('development --remote', () => {
describe({ skip: process.platform === 'darwin' }, 'development --remote', () => {
let devServer;
let remoteDbServer;

Expand Down
10 changes: 10 additions & 0 deletions packages/db/test/fixtures/libsql-remote/astro.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import db from '@astrojs/db';
import { defineConfig } from 'astro/config';

// https://astro.build/config
export default defineConfig({
integrations: [db()],
devToolbar: {
enabled: false,
},
});
13 changes: 13 additions & 0 deletions packages/db/test/fixtures/libsql-remote/db/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { column, defineDb, defineTable } from 'astro:db';

const User = defineTable({
columns: {
id: column.text({ primaryKey: true, optional: false }),
username: column.text({ optional: false, unique: true }),
password: column.text({ optional: false }),
},
});

export default defineDb({
tables: { User },
});
7 changes: 7 additions & 0 deletions packages/db/test/fixtures/libsql-remote/db/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { User, db } from 'astro:db';

export default async function () {
await db.batch([
db.insert(User).values([{ id: 'mario', username: 'Mario', password: 'itsame' }]),
]);
}
14 changes: 14 additions & 0 deletions packages/db/test/fixtures/libsql-remote/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "@test/db-libsql-remote",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"@astrojs/db": "workspace:*",
"astro": "workspace:*"
}
}
11 changes: 11 additions & 0 deletions packages/db/test/fixtures/libsql-remote/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
/// <reference path="../../.astro/db-types.d.ts" />
import { User, db } from 'astro:db';
const users = await db.select().from(User);
---

<h2>Users</h2>
<ul class="users-list">
{users.map((user) => <li>{user.name}</li>)}
</ul>
77 changes: 77 additions & 0 deletions packages/db/test/libsql-remote.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import assert from 'node:assert/strict';
import { relative } from 'node:path';
import { rm } from 'node:fs/promises';
import { after, before, describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';
import testAdapter from '../../astro/test/test-adapter.js';
import { loadFixture } from '../../astro/test/test-utils.js';
import { clearEnvironment, initializeRemoteDb } from './test-utils.js';

describe('astro:db local database', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: new URL('./fixtures/libsql-remote/', import.meta.url),
output: 'server',
adapter: testAdapter(),
});
});

describe('build --remote with local libSQL file (absolute path)', () => {
before(async () => {
clearEnvironment();

const absoluteFileUrl = new URL('./fixtures/libsql-remote/dist/absolute.db', import.meta.url);
// Remove the file if it exists to avoid conflict between test runs
await rm(absoluteFileUrl, { force: true });

process.env.ASTRO_INTERNAL_TEST_REMOTE = true;
process.env.ASTRO_DB_REMOTE_URL = absoluteFileUrl.toString();
await fixture.build();
await initializeRemoteDb(fixture.config);
});

after(async () => {
delete process.env.ASTRO_INTERNAL_TEST_REMOTE;
delete process.env.ASTRO_DB_REMOTE_URL;
});

it('Can render page', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/');
const response = await app.render(request);
assert.equal(response.status, 200);
});
});

describe('build --remote with local libSQL file (relative path)', () => {
before(async () => {
clearEnvironment();

const absoluteFileUrl = new URL('./fixtures/libsql-remote/dist/relative.db', import.meta.url);
const prodDbPath = relative(
fileURLToPath(fixture.config.root),
fileURLToPath(absoluteFileUrl),
);
// Remove the file if it exists to avoid conflict between test runs
await rm(prodDbPath, { force: true });

process.env.ASTRO_INTERNAL_TEST_REMOTE = true;
process.env.ASTRO_DB_REMOTE_URL = `file:${prodDbPath}`;
await fixture.build();
await initializeRemoteDb(fixture.config);
});

after(async () => {
delete process.env.ASTRO_INTERNAL_TEST_REMOTE;
delete process.env.ASTRO_DB_REMOTE_URL;
});

it('Can render page', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/');
const response = await app.render(request);
assert.equal(response.status, 200);
});
});
});
17 changes: 17 additions & 0 deletions packages/db/test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ export async function setupRemoteDbServer(astroConfig) {
};
}

export async function initializeRemoteDb(astroConfig) {
await cli({
config: astroConfig,
flags: {
_: [undefined, 'astro', 'db', 'push'],
remote: true,
},
});
await cli({
config: astroConfig,
flags: {
_: [undefined, 'astro', 'db', 'execute', 'db/seed.ts'],
remote: true,
},
});
}

/**
* Clears the environment variables related to Astro DB and Astro Studio.
*/
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading