Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion cli/api/dbadapters/postgres_execution_sql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,10 +240,23 @@ export class PostgresExecutionSql implements IExecutionSql {
? ` include (${index.include.map(c => `"${c}"`).join(", ")})`
: "";
const where = index.where ? ` where (${index.where})` : "";
return `create ${unique}index "${index.name}" on ${target} using ${method} (${columns})${include}${where}`;
const indexName =
index.name || this.defaultIndexName(table.target.name, index.columns, !!index.unique);
return `create ${unique}index "${indexName}" on ${target} using ${method} (${columns})${include}${where}`;
});
}

// When an index config omits `name`, derive one from the table + columns
// (mirroring Postgres's own `<table>_<col>_idx` default), truncated to the
// 63-char identifier limit. Without this, an unnamed index emits
// `create index "" ...` -> "zero-length delimited identifier".
private defaultIndexName(tableName: string, columns: string[], unique: boolean): string {
const parts = [tableName, ...(columns || [])].filter(Boolean);
const base = parts.join("_");
const suffix = unique ? "_key" : "_idx";
return `${base}${suffix}`.slice(0, 63);
}

private indexMethodAsSql(method?: sqlanvil.PostgresOptions.Index.Method): string {
switch (method) {
case sqlanvil.PostgresOptions.Index.Method.HASH:
Expand Down
26 changes: 26 additions & 0 deletions cli/api/execution_sql_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -216,6 +216,32 @@ suite("ExecutionSql with Postgres/Supabase", () => {
);
});

test("create index without a name derives one (no zero-length identifier)", () => {
const table: sqlanvil.ITable = {
...baseTable,
postgres: {
indexes: [
{ columns: ["id"] },
{ columns: ["field1", "id"], unique: true }
]
}
};
const statements = executionSql
.publishTasks(table, { fullRefresh: false })
.build()
.map(t => t.statement);
// Names are derived as <table>_<cols>_idx (or _key for unique) -- never "".
expect(statements).to.not.include.members([
'create index "" on "my_db"."public"."my_table" using btree ("id")'
]);
expect(statements[2]).to.equal(
'create index "my_table_id_idx" on "my_db"."public"."my_table" using btree ("id")'
);
expect(statements[3]).to.equal(
'create unique index "my_table_field1_id_key" on "my_db"."public"."my_table" using btree ("field1", "id")'
);
});

test("incremental fresh-create emits postgres indexes", () => {
const incTable: sqlanvil.ITable = {
...baseTable,
Expand Down
40 changes: 22 additions & 18 deletions cli/api/utils/postgres.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,18 +65,32 @@ export class PgPoolExecutor {
}) => Promise<T>
) {
const client = await this.pool.connect();
// The client can be released from several places: the client "error" handler,
// the query "error" handler (which passes the error to release() so the bad
// connection is closed, not returned to the pool), and the finally block.
// Release exactly once -- otherwise pg-pool's throwOnDoubleRelease fires and a
// confusing "Release called on client which has already been released" surfaces
// ahead of the real error on every failing statement.
let released = false;
const releaseOnce = (err?: Error) => {
if (released) {
return;
}
released = true;
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing pg.Client", e.message, e.stack);
}
};
try {
client.on("error", err => {
// tslint:disable-next-line: no-console
console.error("pg.Client client error", err.message, err.stack);
// Errored connections cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Client", e.message, e.stack);
}
releaseOnce(err);
});

return await callback({
Expand DownExpand Up@@ -114,12 +128,7 @@ export class PgPoolExecutor {
// Errors don't cause "end" to fire, additionally errored connections
// cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Query", e.message, e.stack);
}
releaseOnce(err);
reject(err);
});
query.on("end", () => {
Expand All@@ -129,12 +138,7 @@ export class PgPoolExecutor {
}
});
} finally {
try {
client.release();
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing ended pg.Client", e.message, e.stack);
}
releaseOnce();
}
}

Expand Down
50 changes: 50 additions & 0 deletions tests/integration/postgres.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,56 @@ suite("@sqlanvil/integration/postgres", { parallel: true }, ({ before, after })
);
});

test("a failing statement rejects with the real error and no double-release noise", async () => {
// Capture console.error so we can assert the pg client isn't released twice
// (which would print "Release called on client which has already been released").
// tslint:disable-next-line: no-console
const originalError = console.error;
const logged: string[] = [];
// tslint:disable-next-line: no-console
console.error = (...args: any[]) => logged.push(args.join(" "));
let err: Error | undefined;
try {
await dbadapter.execute("selct 1");
} catch (e) {
err = e;
} finally {
// tslint:disable-next-line: no-console
console.error = originalError;
}
expect(err, "a bad statement should reject").to.be.an("error");
expect(err.message.toLowerCase()).to.contain("syntax error");
expect(
logged.join("\n"),
"the pg client must be released exactly once"
).to.not.match(/already been released/i);
});

test("a table with an unnamed postgres index creates (derived index name)", async () => {
const schema = "sa_integration_test_unnamed_index";
try {
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
} catch (e) {
// ignore
}
await dbadapter.execute(`create schema "${schema}"`);
const table: sqlanvil.ITable = {
enumType: sqlanvil.TableType.TABLE,
target: { schema, name: "with_idx" },
query: "select 1 as id",
postgres: { indexes: [{ columns: ["id"], unique: true }] }
};
const adapter = new ExecutionSql({ warehouse: "postgres" }, "1.4.8");
for (const task of adapter.publishTasks(table, { fullRefresh: true }).build()) {
await dbadapter.execute(task.statement);
}
const { rows } = await dbadapter.execute(
`select indexname from pg_indexes where schemaname = '${schema}' and tablename = 'with_idx'`
);
expect(rows.map((r: any) => r.indexname)).to.include("with_idx_id_key");
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
});

test("run", { timeout: 60000 }, async () => {
const compiledGraph = await compile("tests/integration/postgres_project", "project_e2e");

Expand Down
2 changes: 1 addition & 1 deletion version.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@
# SemVer line). DF_VERSION is the upstream dataform-co/dataform release this fork
# is synced to — surfaced as metadata (e.g. `sqlanvil --version`), not the package
# version. Bump SQLANVIL_VERSION for sqlanvil releases; bump DF_VERSION on upstream syncs.
SQLANVIL_VERSION = "1.4.0"
SQLANVIL_VERSION = "1.4.1"
DF_VERSION = "3.0.59"
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion cli/api/dbadapters/postgres_execution_sql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,10 +240,23 @@ export class PostgresExecutionSql implements IExecutionSql {
? ` include (${index.include.map(c => `"${c}"`).join(", ")})`
: "";
const where = index.where ? ` where (${index.where})` : "";
return `create ${unique}index "${index.name}" on ${target} using ${method} (${columns})${include}${where}`;
const indexName =
index.name || this.defaultIndexName(table.target.name, index.columns, !!index.unique);
return `create ${unique}index "${indexName}" on ${target} using ${method} (${columns})${include}${where}`;
});
}

// When an index config omits `name`, derive one from the table + columns
// (mirroring Postgres's own `<table>_<col>_idx` default), truncated to the
// 63-char identifier limit. Without this, an unnamed index emits
// `create index "" ...` -> "zero-length delimited identifier".
private defaultIndexName(tableName: string, columns: string[], unique: boolean): string {
const parts = [tableName, ...(columns || [])].filter(Boolean);
const base = parts.join("_");
const suffix = unique ? "_key" : "_idx";
return `${base}${suffix}`.slice(0, 63);
}

private indexMethodAsSql(method?: sqlanvil.PostgresOptions.Index.Method): string {
switch (method) {
case sqlanvil.PostgresOptions.Index.Method.HASH:
Expand Down
26 changes: 26 additions & 0 deletions cli/api/execution_sql_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -216,6 +216,32 @@ suite("ExecutionSql with Postgres/Supabase", () => {
);
});

test("create index without a name derives one (no zero-length identifier)", () => {
const table: sqlanvil.ITable = {
...baseTable,
postgres: {
indexes: [
{ columns: ["id"] },
{ columns: ["field1", "id"], unique: true }
]
}
};
const statements = executionSql
.publishTasks(table, { fullRefresh: false })
.build()
.map(t => t.statement);
// Names are derived as <table>_<cols>_idx (or _key for unique) -- never "".
expect(statements).to.not.include.members([
'create index "" on "my_db"."public"."my_table" using btree ("id")'
]);
expect(statements[2]).to.equal(
'create index "my_table_id_idx" on "my_db"."public"."my_table" using btree ("id")'
);
expect(statements[3]).to.equal(
'create unique index "my_table_field1_id_key" on "my_db"."public"."my_table" using btree ("field1", "id")'
);
});

test("incremental fresh-create emits postgres indexes", () => {
const incTable: sqlanvil.ITable = {
...baseTable,
Expand Down
40 changes: 22 additions & 18 deletions cli/api/utils/postgres.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,18 +65,32 @@ export class PgPoolExecutor {
}) => Promise<T>
) {
const client = await this.pool.connect();
// The client can be released from several places: the client "error" handler,
// the query "error" handler (which passes the error to release() so the bad
// connection is closed, not returned to the pool), and the finally block.
// Release exactly once -- otherwise pg-pool's throwOnDoubleRelease fires and a
// confusing "Release called on client which has already been released" surfaces
// ahead of the real error on every failing statement.
let released = false;
const releaseOnce = (err?: Error) => {
if (released) {
return;
}
released = true;
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing pg.Client", e.message, e.stack);
}
};
try {
client.on("error", err => {
// tslint:disable-next-line: no-console
console.error("pg.Client client error", err.message, err.stack);
// Errored connections cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Client", e.message, e.stack);
}
releaseOnce(err);
});

return await callback({
Expand DownExpand Up@@ -114,12 +128,7 @@ export class PgPoolExecutor {
// Errors don't cause "end" to fire, additionally errored connections
// cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Query", e.message, e.stack);
}
releaseOnce(err);
reject(err);
});
query.on("end", () => {
Expand All@@ -129,12 +138,7 @@ export class PgPoolExecutor {
}
});
} finally {
try {
client.release();
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing ended pg.Client", e.message, e.stack);
}
releaseOnce();
}
}

Expand Down
50 changes: 50 additions & 0 deletions tests/integration/postgres.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,56 @@ suite("@sqlanvil/integration/postgres", { parallel: true }, ({ before, after })
);
});

test("a failing statement rejects with the real error and no double-release noise", async () => {
// Capture console.error so we can assert the pg client isn't released twice
// (which would print "Release called on client which has already been released").
// tslint:disable-next-line: no-console
const originalError = console.error;
const logged: string[] = [];
// tslint:disable-next-line: no-console
console.error = (...args: any[]) => logged.push(args.join(" "));
let err: Error | undefined;
try {
await dbadapter.execute("selct 1");
} catch (e) {
err = e;
} finally {
// tslint:disable-next-line: no-console
console.error = originalError;
}
expect(err, "a bad statement should reject").to.be.an("error");
expect(err.message.toLowerCase()).to.contain("syntax error");
expect(
logged.join("\n"),
"the pg client must be released exactly once"
).to.not.match(/already been released/i);
});

test("a table with an unnamed postgres index creates (derived index name)", async () => {
const schema = "sa_integration_test_unnamed_index";
try {
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
} catch (e) {
// ignore
}
await dbadapter.execute(`create schema "${schema}"`);
const table: sqlanvil.ITable = {
enumType: sqlanvil.TableType.TABLE,
target: { schema, name: "with_idx" },
query: "select 1 as id",
postgres: { indexes: [{ columns: ["id"], unique: true }] }
};
const adapter = new ExecutionSql({ warehouse: "postgres" }, "1.4.8");
for (const task of adapter.publishTasks(table, { fullRefresh: true }).build()) {
await dbadapter.execute(task.statement);
}
const { rows } = await dbadapter.execute(
`select indexname from pg_indexes where schemaname = '${schema}' and tablename = 'with_idx'`
);
expect(rows.map((r: any) => r.indexname)).to.include("with_idx_id_key");
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
});

test("run", { timeout: 60000 }, async () => {
const compiledGraph = await compile("tests/integration/postgres_project", "project_e2e");

Expand Down
2 changes: 1 addition & 1 deletion version.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@
# SemVer line). DF_VERSION is the upstream dataform-co/dataform release this fork
# is synced to — surfaced as metadata (e.g. `sqlanvil --version`), not the package
# version. Bump SQLANVIL_VERSION for sqlanvil releases; bump DF_VERSION on upstream syncs.
SQLANVIL_VERSION = "1.4.0"
SQLANVIL_VERSION = "1.4.1"
DF_VERSION = "3.0.59"
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion cli/api/dbadapters/postgres_execution_sql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,10 +240,23 @@ export class PostgresExecutionSql implements IExecutionSql {
? ` include (${index.include.map(c => `"${c}"`).join(", ")})`
: "";
const where = index.where ? ` where (${index.where})` : "";
return `create ${unique}index "${index.name}" on ${target} using ${method} (${columns})${include}${where}`;
const indexName =
index.name || this.defaultIndexName(table.target.name, index.columns, !!index.unique);
return `create ${unique}index "${indexName}" on ${target} using ${method} (${columns})${include}${where}`;
});
}

// When an index config omits `name`, derive one from the table + columns
// (mirroring Postgres's own `<table>_<col>_idx` default), truncated to the
// 63-char identifier limit. Without this, an unnamed index emits
// `create index "" ...` -> "zero-length delimited identifier".
private defaultIndexName(tableName: string, columns: string[], unique: boolean): string {
const parts = [tableName, ...(columns || [])].filter(Boolean);
const base = parts.join("_");
const suffix = unique ? "_key" : "_idx";
return `${base}${suffix}`.slice(0, 63);
}

private indexMethodAsSql(method?: sqlanvil.PostgresOptions.Index.Method): string {
switch (method) {
case sqlanvil.PostgresOptions.Index.Method.HASH:
Expand Down
26 changes: 26 additions & 0 deletions cli/api/execution_sql_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -216,6 +216,32 @@ suite("ExecutionSql with Postgres/Supabase", () => {
);
});

test("create index without a name derives one (no zero-length identifier)", () => {
const table: sqlanvil.ITable = {
...baseTable,
postgres: {
indexes: [
{ columns: ["id"] },
{ columns: ["field1", "id"], unique: true }
]
}
};
const statements = executionSql
.publishTasks(table, { fullRefresh: false })
.build()
.map(t => t.statement);
// Names are derived as <table>_<cols>_idx (or _key for unique) -- never "".
expect(statements).to.not.include.members([
'create index "" on "my_db"."public"."my_table" using btree ("id")'
]);
expect(statements[2]).to.equal(
'create index "my_table_id_idx" on "my_db"."public"."my_table" using btree ("id")'
);
expect(statements[3]).to.equal(
'create unique index "my_table_field1_id_key" on "my_db"."public"."my_table" using btree ("field1", "id")'
);
});

test("incremental fresh-create emits postgres indexes", () => {
const incTable: sqlanvil.ITable = {
...baseTable,
Expand Down
40 changes: 22 additions & 18 deletions cli/api/utils/postgres.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,18 +65,32 @@ export class PgPoolExecutor {
}) => Promise<T>
) {
const client = await this.pool.connect();
// The client can be released from several places: the client "error" handler,
// the query "error" handler (which passes the error to release() so the bad
// connection is closed, not returned to the pool), and the finally block.
// Release exactly once -- otherwise pg-pool's throwOnDoubleRelease fires and a
// confusing "Release called on client which has already been released" surfaces
// ahead of the real error on every failing statement.
let released = false;
const releaseOnce = (err?: Error) => {
if (released) {
return;
}
released = true;
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing pg.Client", e.message, e.stack);
}
};
try {
client.on("error", err => {
// tslint:disable-next-line: no-console
console.error("pg.Client client error", err.message, err.stack);
// Errored connections cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Client", e.message, e.stack);
}
releaseOnce(err);
});

return await callback({
Expand DownExpand Up@@ -114,12 +128,7 @@ export class PgPoolExecutor {
// Errors don't cause "end" to fire, additionally errored connections
// cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Query", e.message, e.stack);
}
releaseOnce(err);
reject(err);
});
query.on("end", () => {
Expand All@@ -129,12 +138,7 @@ export class PgPoolExecutor {
}
});
} finally {
try {
client.release();
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing ended pg.Client", e.message, e.stack);
}
releaseOnce();
}
}

Expand Down
50 changes: 50 additions & 0 deletions tests/integration/postgres.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,56 @@ suite("@sqlanvil/integration/postgres", { parallel: true }, ({ before, after })
);
});

test("a failing statement rejects with the real error and no double-release noise", async () => {
// Capture console.error so we can assert the pg client isn't released twice
// (which would print "Release called on client which has already been released").
// tslint:disable-next-line: no-console
const originalError = console.error;
const logged: string[] = [];
// tslint:disable-next-line: no-console
console.error = (...args: any[]) => logged.push(args.join(" "));
let err: Error | undefined;
try {
await dbadapter.execute("selct 1");
} catch (e) {
err = e;
} finally {
// tslint:disable-next-line: no-console
console.error = originalError;
}
expect(err, "a bad statement should reject").to.be.an("error");
expect(err.message.toLowerCase()).to.contain("syntax error");
expect(
logged.join("\n"),
"the pg client must be released exactly once"
).to.not.match(/already been released/i);
});

test("a table with an unnamed postgres index creates (derived index name)", async () => {
const schema = "sa_integration_test_unnamed_index";
try {
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
} catch (e) {
// ignore
}
await dbadapter.execute(`create schema "${schema}"`);
const table: sqlanvil.ITable = {
enumType: sqlanvil.TableType.TABLE,
target: { schema, name: "with_idx" },
query: "select 1 as id",
postgres: { indexes: [{ columns: ["id"], unique: true }] }
};
const adapter = new ExecutionSql({ warehouse: "postgres" }, "1.4.8");
for (const task of adapter.publishTasks(table, { fullRefresh: true }).build()) {
await dbadapter.execute(task.statement);
}
const { rows } = await dbadapter.execute(
`select indexname from pg_indexes where schemaname = '${schema}' and tablename = 'with_idx'`
);
expect(rows.map((r: any) => r.indexname)).to.include("with_idx_id_key");
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
});

test("run", { timeout: 60000 }, async () => {
const compiledGraph = await compile("tests/integration/postgres_project", "project_e2e");

Expand Down
2 changes: 1 addition & 1 deletion version.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@
# SemVer line). DF_VERSION is the upstream dataform-co/dataform release this fork
# is synced to — surfaced as metadata (e.g. `sqlanvil --version`), not the package
# version. Bump SQLANVIL_VERSION for sqlanvil releases; bump DF_VERSION on upstream syncs.
SQLANVIL_VERSION = "1.4.0"
SQLANVIL_VERSION = "1.4.1"
DF_VERSION = "3.0.59"
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion cli/api/dbadapters/postgres_execution_sql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,10 +240,23 @@ export class PostgresExecutionSql implements IExecutionSql {
? ` include (${index.include.map(c => `"${c}"`).join(", ")})`
: "";
const where = index.where ? ` where (${index.where})` : "";
return `create ${unique}index "${index.name}" on ${target} using ${method} (${columns})${include}${where}`;
const indexName =
index.name || this.defaultIndexName(table.target.name, index.columns, !!index.unique);
return `create ${unique}index "${indexName}" on ${target} using ${method} (${columns})${include}${where}`;
});
}

// When an index config omits `name`, derive one from the table + columns
// (mirroring Postgres's own `<table>_<col>_idx` default), truncated to the
// 63-char identifier limit. Without this, an unnamed index emits
// `create index "" ...` -> "zero-length delimited identifier".
private defaultIndexName(tableName: string, columns: string[], unique: boolean): string {
const parts = [tableName, ...(columns || [])].filter(Boolean);
const base = parts.join("_");
const suffix = unique ? "_key" : "_idx";
return `${base}${suffix}`.slice(0, 63);
}

private indexMethodAsSql(method?: sqlanvil.PostgresOptions.Index.Method): string {
switch (method) {
case sqlanvil.PostgresOptions.Index.Method.HASH:
Expand Down
26 changes: 26 additions & 0 deletions cli/api/execution_sql_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -216,6 +216,32 @@ suite("ExecutionSql with Postgres/Supabase", () => {
);
});

test("create index without a name derives one (no zero-length identifier)", () => {
const table: sqlanvil.ITable = {
...baseTable,
postgres: {
indexes: [
{ columns: ["id"] },
{ columns: ["field1", "id"], unique: true }
]
}
};
const statements = executionSql
.publishTasks(table, { fullRefresh: false })
.build()
.map(t => t.statement);
// Names are derived as <table>_<cols>_idx (or _key for unique) -- never "".
expect(statements).to.not.include.members([
'create index "" on "my_db"."public"."my_table" using btree ("id")'
]);
expect(statements[2]).to.equal(
'create index "my_table_id_idx" on "my_db"."public"."my_table" using btree ("id")'
);
expect(statements[3]).to.equal(
'create unique index "my_table_field1_id_key" on "my_db"."public"."my_table" using btree ("field1", "id")'
);
});

test("incremental fresh-create emits postgres indexes", () => {
const incTable: sqlanvil.ITable = {
...baseTable,
Expand Down
40 changes: 22 additions & 18 deletions cli/api/utils/postgres.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,18 +65,32 @@ export class PgPoolExecutor {
}) => Promise<T>
) {
const client = await this.pool.connect();
// The client can be released from several places: the client "error" handler,
// the query "error" handler (which passes the error to release() so the bad
// connection is closed, not returned to the pool), and the finally block.
// Release exactly once -- otherwise pg-pool's throwOnDoubleRelease fires and a
// confusing "Release called on client which has already been released" surfaces
// ahead of the real error on every failing statement.
let released = false;
const releaseOnce = (err?: Error) => {
if (released) {
return;
}
released = true;
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing pg.Client", e.message, e.stack);
}
};
try {
client.on("error", err => {
// tslint:disable-next-line: no-console
console.error("pg.Client client error", err.message, err.stack);
// Errored connections cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Client", e.message, e.stack);
}
releaseOnce(err);
});

return await callback({
Expand DownExpand Up@@ -114,12 +128,7 @@ export class PgPoolExecutor {
// Errors don't cause "end" to fire, additionally errored connections
// cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Query", e.message, e.stack);
}
releaseOnce(err);
reject(err);
});
query.on("end", () => {
Expand All@@ -129,12 +138,7 @@ export class PgPoolExecutor {
}
});
} finally {
try {
client.release();
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing ended pg.Client", e.message, e.stack);
}
releaseOnce();
}
}

Expand Down
50 changes: 50 additions & 0 deletions tests/integration/postgres.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,56 @@ suite("@sqlanvil/integration/postgres", { parallel: true }, ({ before, after })
);
});

test("a failing statement rejects with the real error and no double-release noise", async () => {
// Capture console.error so we can assert the pg client isn't released twice
// (which would print "Release called on client which has already been released").
// tslint:disable-next-line: no-console
const originalError = console.error;
const logged: string[] = [];
// tslint:disable-next-line: no-console
console.error = (...args: any[]) => logged.push(args.join(" "));
let err: Error | undefined;
try {
await dbadapter.execute("selct 1");
} catch (e) {
err = e;
} finally {
// tslint:disable-next-line: no-console
console.error = originalError;
}
expect(err, "a bad statement should reject").to.be.an("error");
expect(err.message.toLowerCase()).to.contain("syntax error");
expect(
logged.join("\n"),
"the pg client must be released exactly once"
).to.not.match(/already been released/i);
});

test("a table with an unnamed postgres index creates (derived index name)", async () => {
const schema = "sa_integration_test_unnamed_index";
try {
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
} catch (e) {
// ignore
}
await dbadapter.execute(`create schema "${schema}"`);
const table: sqlanvil.ITable = {
enumType: sqlanvil.TableType.TABLE,
target: { schema, name: "with_idx" },
query: "select 1 as id",
postgres: { indexes: [{ columns: ["id"], unique: true }] }
};
const adapter = new ExecutionSql({ warehouse: "postgres" }, "1.4.8");
for (const task of adapter.publishTasks(table, { fullRefresh: true }).build()) {
await dbadapter.execute(task.statement);
}
const { rows } = await dbadapter.execute(
`select indexname from pg_indexes where schemaname = '${schema}' and tablename = 'with_idx'`
);
expect(rows.map((r: any) => r.indexname)).to.include("with_idx_id_key");
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
});

test("run", { timeout: 60000 }, async () => {
const compiledGraph = await compile("tests/integration/postgres_project", "project_e2e");

Expand Down
2 changes: 1 addition & 1 deletion version.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@
# SemVer line). DF_VERSION is the upstream dataform-co/dataform release this fork
# is synced to — surfaced as metadata (e.g. `sqlanvil --version`), not the package
# version. Bump SQLANVIL_VERSION for sqlanvil releases; bump DF_VERSION on upstream syncs.
SQLANVIL_VERSION = "1.4.0"
SQLANVIL_VERSION = "1.4.1"
DF_VERSION = "3.0.59"
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion cli/api/dbadapters/postgres_execution_sql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,10 +240,23 @@ export class PostgresExecutionSql implements IExecutionSql {
? ` include (${index.include.map(c => `"${c}"`).join(", ")})`
: "";
const where = index.where ? ` where (${index.where})` : "";
return `create ${unique}index "${index.name}" on ${target} using ${method} (${columns})${include}${where}`;
const indexName =
index.name || this.defaultIndexName(table.target.name, index.columns, !!index.unique);
return `create ${unique}index "${indexName}" on ${target} using ${method} (${columns})${include}${where}`;
});
}

// When an index config omits `name`, derive one from the table + columns
// (mirroring Postgres's own `<table>_<col>_idx` default), truncated to the
// 63-char identifier limit. Without this, an unnamed index emits
// `create index "" ...` -> "zero-length delimited identifier".
private defaultIndexName(tableName: string, columns: string[], unique: boolean): string {
const parts = [tableName, ...(columns || [])].filter(Boolean);
const base = parts.join("_");
const suffix = unique ? "_key" : "_idx";
return `${base}${suffix}`.slice(0, 63);
}

private indexMethodAsSql(method?: sqlanvil.PostgresOptions.Index.Method): string {
switch (method) {
case sqlanvil.PostgresOptions.Index.Method.HASH:
Expand Down
26 changes: 26 additions & 0 deletions cli/api/execution_sql_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -216,6 +216,32 @@ suite("ExecutionSql with Postgres/Supabase", () => {
);
});

test("create index without a name derives one (no zero-length identifier)", () => {
const table: sqlanvil.ITable = {
...baseTable,
postgres: {
indexes: [
{ columns: ["id"] },
{ columns: ["field1", "id"], unique: true }
]
}
};
const statements = executionSql
.publishTasks(table, { fullRefresh: false })
.build()
.map(t => t.statement);
// Names are derived as <table>_<cols>_idx (or _key for unique) -- never "".
expect(statements).to.not.include.members([
'create index "" on "my_db"."public"."my_table" using btree ("id")'
]);
expect(statements[2]).to.equal(
'create index "my_table_id_idx" on "my_db"."public"."my_table" using btree ("id")'
);
expect(statements[3]).to.equal(
'create unique index "my_table_field1_id_key" on "my_db"."public"."my_table" using btree ("field1", "id")'
);
});

test("incremental fresh-create emits postgres indexes", () => {
const incTable: sqlanvil.ITable = {
...baseTable,
Expand Down
40 changes: 22 additions & 18 deletions cli/api/utils/postgres.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,18 +65,32 @@ export class PgPoolExecutor {
}) => Promise<T>
) {
const client = await this.pool.connect();
// The client can be released from several places: the client "error" handler,
// the query "error" handler (which passes the error to release() so the bad
// connection is closed, not returned to the pool), and the finally block.
// Release exactly once -- otherwise pg-pool's throwOnDoubleRelease fires and a
// confusing "Release called on client which has already been released" surfaces
// ahead of the real error on every failing statement.
let released = false;
const releaseOnce = (err?: Error) => {
if (released) {
return;
}
released = true;
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing pg.Client", e.message, e.stack);
}
};
try {
client.on("error", err => {
// tslint:disable-next-line: no-console
console.error("pg.Client client error", err.message, err.stack);
// Errored connections cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Client", e.message, e.stack);
}
releaseOnce(err);
});

return await callback({
Expand DownExpand Up@@ -114,12 +128,7 @@ export class PgPoolExecutor {
// Errors don't cause "end" to fire, additionally errored connections
// cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Query", e.message, e.stack);
}
releaseOnce(err);
reject(err);
});
query.on("end", () => {
Expand All@@ -129,12 +138,7 @@ export class PgPoolExecutor {
}
});
} finally {
try {
client.release();
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing ended pg.Client", e.message, e.stack);
}
releaseOnce();
}
}

Expand Down
50 changes: 50 additions & 0 deletions tests/integration/postgres.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,56 @@ suite("@sqlanvil/integration/postgres", { parallel: true }, ({ before, after })
);
});

test("a failing statement rejects with the real error and no double-release noise", async () => {
// Capture console.error so we can assert the pg client isn't released twice
// (which would print "Release called on client which has already been released").
// tslint:disable-next-line: no-console
const originalError = console.error;
const logged: string[] = [];
// tslint:disable-next-line: no-console
console.error = (...args: any[]) => logged.push(args.join(" "));
let err: Error | undefined;
try {
await dbadapter.execute("selct 1");
} catch (e) {
err = e;
} finally {
// tslint:disable-next-line: no-console
console.error = originalError;
}
expect(err, "a bad statement should reject").to.be.an("error");
expect(err.message.toLowerCase()).to.contain("syntax error");
expect(
logged.join("\n"),
"the pg client must be released exactly once"
).to.not.match(/already been released/i);
});

test("a table with an unnamed postgres index creates (derived index name)", async () => {
const schema = "sa_integration_test_unnamed_index";
try {
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
} catch (e) {
// ignore
}
await dbadapter.execute(`create schema "${schema}"`);
const table: sqlanvil.ITable = {
enumType: sqlanvil.TableType.TABLE,
target: { schema, name: "with_idx" },
query: "select 1 as id",
postgres: { indexes: [{ columns: ["id"], unique: true }] }
};
const adapter = new ExecutionSql({ warehouse: "postgres" }, "1.4.8");
for (const task of adapter.publishTasks(table, { fullRefresh: true }).build()) {
await dbadapter.execute(task.statement);
}
const { rows } = await dbadapter.execute(
`select indexname from pg_indexes where schemaname = '${schema}' and tablename = 'with_idx'`
);
expect(rows.map((r: any) => r.indexname)).to.include("with_idx_id_key");
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
});

test("run", { timeout: 60000 }, async () => {
const compiledGraph = await compile("tests/integration/postgres_project", "project_e2e");

Expand Down
2 changes: 1 addition & 1 deletion version.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@
# SemVer line). DF_VERSION is the upstream dataform-co/dataform release this fork
# is synced to — surfaced as metadata (e.g. `sqlanvil --version`), not the package
# version. Bump SQLANVIL_VERSION for sqlanvil releases; bump DF_VERSION on upstream syncs.
SQLANVIL_VERSION = "1.4.0"
SQLANVIL_VERSION = "1.4.1"
DF_VERSION = "3.0.59"
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion cli/api/dbadapters/postgres_execution_sql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,10 +240,23 @@ export class PostgresExecutionSql implements IExecutionSql {
? ` include (${index.include.map(c => `"${c}"`).join(", ")})`
: "";
const where = index.where ? ` where (${index.where})` : "";
return `create ${unique}index "${index.name}" on ${target} using ${method} (${columns})${include}${where}`;
const indexName =
index.name || this.defaultIndexName(table.target.name, index.columns, !!index.unique);
return `create ${unique}index "${indexName}" on ${target} using ${method} (${columns})${include}${where}`;
});
}

// When an index config omits `name`, derive one from the table + columns
// (mirroring Postgres's own `<table>_<col>_idx` default), truncated to the
// 63-char identifier limit. Without this, an unnamed index emits
// `create index "" ...` -> "zero-length delimited identifier".
private defaultIndexName(tableName: string, columns: string[], unique: boolean): string {
const parts = [tableName, ...(columns || [])].filter(Boolean);
const base = parts.join("_");
const suffix = unique ? "_key" : "_idx";
return `${base}${suffix}`.slice(0, 63);
}

private indexMethodAsSql(method?: sqlanvil.PostgresOptions.Index.Method): string {
switch (method) {
case sqlanvil.PostgresOptions.Index.Method.HASH:
Expand Down
26 changes: 26 additions & 0 deletions cli/api/execution_sql_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -216,6 +216,32 @@ suite("ExecutionSql with Postgres/Supabase", () => {
);
});

test("create index without a name derives one (no zero-length identifier)", () => {
const table: sqlanvil.ITable = {
...baseTable,
postgres: {
indexes: [
{ columns: ["id"] },
{ columns: ["field1", "id"], unique: true }
]
}
};
const statements = executionSql
.publishTasks(table, { fullRefresh: false })
.build()
.map(t => t.statement);
// Names are derived as <table>_<cols>_idx (or _key for unique) -- never "".
expect(statements).to.not.include.members([
'create index "" on "my_db"."public"."my_table" using btree ("id")'
]);
expect(statements[2]).to.equal(
'create index "my_table_id_idx" on "my_db"."public"."my_table" using btree ("id")'
);
expect(statements[3]).to.equal(
'create unique index "my_table_field1_id_key" on "my_db"."public"."my_table" using btree ("field1", "id")'
);
});

test("incremental fresh-create emits postgres indexes", () => {
const incTable: sqlanvil.ITable = {
...baseTable,
Expand Down
40 changes: 22 additions & 18 deletions cli/api/utils/postgres.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,18 +65,32 @@ export class PgPoolExecutor {
}) => Promise<T>
) {
const client = await this.pool.connect();
// The client can be released from several places: the client "error" handler,
// the query "error" handler (which passes the error to release() so the bad
// connection is closed, not returned to the pool), and the finally block.
// Release exactly once -- otherwise pg-pool's throwOnDoubleRelease fires and a
// confusing "Release called on client which has already been released" surfaces
// ahead of the real error on every failing statement.
let released = false;
const releaseOnce = (err?: Error) => {
if (released) {
return;
}
released = true;
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing pg.Client", e.message, e.stack);
}
};
try {
client.on("error", err => {
// tslint:disable-next-line: no-console
console.error("pg.Client client error", err.message, err.stack);
// Errored connections cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Client", e.message, e.stack);
}
releaseOnce(err);
});

return await callback({
Expand DownExpand Up@@ -114,12 +128,7 @@ export class PgPoolExecutor {
// Errors don't cause "end" to fire, additionally errored connections
// cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Query", e.message, e.stack);
}
releaseOnce(err);
reject(err);
});
query.on("end", () => {
Expand All@@ -129,12 +138,7 @@ export class PgPoolExecutor {
}
});
} finally {
try {
client.release();
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing ended pg.Client", e.message, e.stack);
}
releaseOnce();
}
}

Expand Down
50 changes: 50 additions & 0 deletions tests/integration/postgres.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,56 @@ suite("@sqlanvil/integration/postgres", { parallel: true }, ({ before, after })
);
});

test("a failing statement rejects with the real error and no double-release noise", async () => {
// Capture console.error so we can assert the pg client isn't released twice
// (which would print "Release called on client which has already been released").
// tslint:disable-next-line: no-console
const originalError = console.error;
const logged: string[] = [];
// tslint:disable-next-line: no-console
console.error = (...args: any[]) => logged.push(args.join(" "));
let err: Error | undefined;
try {
await dbadapter.execute("selct 1");
} catch (e) {
err = e;
} finally {
// tslint:disable-next-line: no-console
console.error = originalError;
}
expect(err, "a bad statement should reject").to.be.an("error");
expect(err.message.toLowerCase()).to.contain("syntax error");
expect(
logged.join("\n"),
"the pg client must be released exactly once"
).to.not.match(/already been released/i);
});

test("a table with an unnamed postgres index creates (derived index name)", async () => {
const schema = "sa_integration_test_unnamed_index";
try {
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
} catch (e) {
// ignore
}
await dbadapter.execute(`create schema "${schema}"`);
const table: sqlanvil.ITable = {
enumType: sqlanvil.TableType.TABLE,
target: { schema, name: "with_idx" },
query: "select 1 as id",
postgres: { indexes: [{ columns: ["id"], unique: true }] }
};
const adapter = new ExecutionSql({ warehouse: "postgres" }, "1.4.8");
for (const task of adapter.publishTasks(table, { fullRefresh: true }).build()) {
await dbadapter.execute(task.statement);
}
const { rows } = await dbadapter.execute(
`select indexname from pg_indexes where schemaname = '${schema}' and tablename = 'with_idx'`
);
expect(rows.map((r: any) => r.indexname)).to.include("with_idx_id_key");
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
});

test("run", { timeout: 60000 }, async () => {
const compiledGraph = await compile("tests/integration/postgres_project", "project_e2e");

Expand Down
2 changes: 1 addition & 1 deletion version.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@
# SemVer line). DF_VERSION is the upstream dataform-co/dataform release this fork
# is synced to — surfaced as metadata (e.g. `sqlanvil --version`), not the package
# version. Bump SQLANVIL_VERSION for sqlanvil releases; bump DF_VERSION on upstream syncs.
SQLANVIL_VERSION = "1.4.0"
SQLANVIL_VERSION = "1.4.1"
DF_VERSION = "3.0.59"
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion cli/api/dbadapters/postgres_execution_sql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,10 +240,23 @@ export class PostgresExecutionSql implements IExecutionSql {
? ` include (${index.include.map(c => `"${c}"`).join(", ")})`
: "";
const where = index.where ? ` where (${index.where})` : "";
return `create ${unique}index "${index.name}" on ${target} using ${method} (${columns})${include}${where}`;
const indexName =
index.name || this.defaultIndexName(table.target.name, index.columns, !!index.unique);
return `create ${unique}index "${indexName}" on ${target} using ${method} (${columns})${include}${where}`;
});
}

// When an index config omits `name`, derive one from the table + columns
// (mirroring Postgres's own `<table>_<col>_idx` default), truncated to the
// 63-char identifier limit. Without this, an unnamed index emits
// `create index "" ...` -> "zero-length delimited identifier".
private defaultIndexName(tableName: string, columns: string[], unique: boolean): string {
const parts = [tableName, ...(columns || [])].filter(Boolean);
const base = parts.join("_");
const suffix = unique ? "_key" : "_idx";
return `${base}${suffix}`.slice(0, 63);
}

private indexMethodAsSql(method?: sqlanvil.PostgresOptions.Index.Method): string {
switch (method) {
case sqlanvil.PostgresOptions.Index.Method.HASH:
Expand Down
26 changes: 26 additions & 0 deletions cli/api/execution_sql_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -216,6 +216,32 @@ suite("ExecutionSql with Postgres/Supabase", () => {
);
});

test("create index without a name derives one (no zero-length identifier)", () => {
const table: sqlanvil.ITable = {
...baseTable,
postgres: {
indexes: [
{ columns: ["id"] },
{ columns: ["field1", "id"], unique: true }
]
}
};
const statements = executionSql
.publishTasks(table, { fullRefresh: false })
.build()
.map(t => t.statement);
// Names are derived as <table>_<cols>_idx (or _key for unique) -- never "".
expect(statements).to.not.include.members([
'create index "" on "my_db"."public"."my_table" using btree ("id")'
]);
expect(statements[2]).to.equal(
'create index "my_table_id_idx" on "my_db"."public"."my_table" using btree ("id")'
);
expect(statements[3]).to.equal(
'create unique index "my_table_field1_id_key" on "my_db"."public"."my_table" using btree ("field1", "id")'
);
});

test("incremental fresh-create emits postgres indexes", () => {
const incTable: sqlanvil.ITable = {
...baseTable,
Expand Down
40 changes: 22 additions & 18 deletions cli/api/utils/postgres.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,18 +65,32 @@ export class PgPoolExecutor {
}) => Promise<T>
) {
const client = await this.pool.connect();
// The client can be released from several places: the client "error" handler,
// the query "error" handler (which passes the error to release() so the bad
// connection is closed, not returned to the pool), and the finally block.
// Release exactly once -- otherwise pg-pool's throwOnDoubleRelease fires and a
// confusing "Release called on client which has already been released" surfaces
// ahead of the real error on every failing statement.
let released = false;
const releaseOnce = (err?: Error) => {
if (released) {
return;
}
released = true;
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing pg.Client", e.message, e.stack);
}
};
try {
client.on("error", err => {
// tslint:disable-next-line: no-console
console.error("pg.Client client error", err.message, err.stack);
// Errored connections cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Client", e.message, e.stack);
}
releaseOnce(err);
});

return await callback({
Expand DownExpand Up@@ -114,12 +128,7 @@ export class PgPoolExecutor {
// Errors don't cause "end" to fire, additionally errored connections
// cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Query", e.message, e.stack);
}
releaseOnce(err);
reject(err);
});
query.on("end", () => {
Expand All@@ -129,12 +138,7 @@ export class PgPoolExecutor {
}
});
} finally {
try {
client.release();
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing ended pg.Client", e.message, e.stack);
}
releaseOnce();
}
}

Expand Down
50 changes: 50 additions & 0 deletions tests/integration/postgres.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,56 @@ suite("@sqlanvil/integration/postgres", { parallel: true }, ({ before, after })
);
});

test("a failing statement rejects with the real error and no double-release noise", async () => {
// Capture console.error so we can assert the pg client isn't released twice
// (which would print "Release called on client which has already been released").
// tslint:disable-next-line: no-console
const originalError = console.error;
const logged: string[] = [];
// tslint:disable-next-line: no-console
console.error = (...args: any[]) => logged.push(args.join(" "));
let err: Error | undefined;
try {
await dbadapter.execute("selct 1");
} catch (e) {
err = e;
} finally {
// tslint:disable-next-line: no-console
console.error = originalError;
}
expect(err, "a bad statement should reject").to.be.an("error");
expect(err.message.toLowerCase()).to.contain("syntax error");
expect(
logged.join("\n"),
"the pg client must be released exactly once"
).to.not.match(/already been released/i);
});

test("a table with an unnamed postgres index creates (derived index name)", async () => {
const schema = "sa_integration_test_unnamed_index";
try {
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
} catch (e) {
// ignore
}
await dbadapter.execute(`create schema "${schema}"`);
const table: sqlanvil.ITable = {
enumType: sqlanvil.TableType.TABLE,
target: { schema, name: "with_idx" },
query: "select 1 as id",
postgres: { indexes: [{ columns: ["id"], unique: true }] }
};
const adapter = new ExecutionSql({ warehouse: "postgres" }, "1.4.8");
for (const task of adapter.publishTasks(table, { fullRefresh: true }).build()) {
await dbadapter.execute(task.statement);
}
const { rows } = await dbadapter.execute(
`select indexname from pg_indexes where schemaname = '${schema}' and tablename = 'with_idx'`
);
expect(rows.map((r: any) => r.indexname)).to.include("with_idx_id_key");
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
});

test("run", { timeout: 60000 }, async () => {
const compiledGraph = await compile("tests/integration/postgres_project", "project_e2e");

Expand Down
2 changes: 1 addition & 1 deletion version.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@
# SemVer line). DF_VERSION is the upstream dataform-co/dataform release this fork
# is synced to — surfaced as metadata (e.g. `sqlanvil --version`), not the package
# version. Bump SQLANVIL_VERSION for sqlanvil releases; bump DF_VERSION on upstream syncs.
SQLANVIL_VERSION = "1.4.0"
SQLANVIL_VERSION = "1.4.1"
DF_VERSION = "3.0.59"
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion cli/api/dbadapters/postgres_execution_sql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,10 +240,23 @@ export class PostgresExecutionSql implements IExecutionSql {
? ` include (${index.include.map(c => `"${c}"`).join(", ")})`
: "";
const where = index.where ? ` where (${index.where})` : "";
return `create ${unique}index "${index.name}" on ${target} using ${method} (${columns})${include}${where}`;
const indexName =
index.name || this.defaultIndexName(table.target.name, index.columns, !!index.unique);
return `create ${unique}index "${indexName}" on ${target} using ${method} (${columns})${include}${where}`;
});
}

// When an index config omits `name`, derive one from the table + columns
// (mirroring Postgres's own `<table>_<col>_idx` default), truncated to the
// 63-char identifier limit. Without this, an unnamed index emits
// `create index "" ...` -> "zero-length delimited identifier".
private defaultIndexName(tableName: string, columns: string[], unique: boolean): string {
const parts = [tableName, ...(columns || [])].filter(Boolean);
const base = parts.join("_");
const suffix = unique ? "_key" : "_idx";
return `${base}${suffix}`.slice(0, 63);
}

private indexMethodAsSql(method?: sqlanvil.PostgresOptions.Index.Method): string {
switch (method) {
case sqlanvil.PostgresOptions.Index.Method.HASH:
Expand Down
26 changes: 26 additions & 0 deletions cli/api/execution_sql_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -216,6 +216,32 @@ suite("ExecutionSql with Postgres/Supabase", () => {
);
});

test("create index without a name derives one (no zero-length identifier)", () => {
const table: sqlanvil.ITable = {
...baseTable,
postgres: {
indexes: [
{ columns: ["id"] },
{ columns: ["field1", "id"], unique: true }
]
}
};
const statements = executionSql
.publishTasks(table, { fullRefresh: false })
.build()
.map(t => t.statement);
// Names are derived as <table>_<cols>_idx (or _key for unique) -- never "".
expect(statements).to.not.include.members([
'create index "" on "my_db"."public"."my_table" using btree ("id")'
]);
expect(statements[2]).to.equal(
'create index "my_table_id_idx" on "my_db"."public"."my_table" using btree ("id")'
);
expect(statements[3]).to.equal(
'create unique index "my_table_field1_id_key" on "my_db"."public"."my_table" using btree ("field1", "id")'
);
});

test("incremental fresh-create emits postgres indexes", () => {
const incTable: sqlanvil.ITable = {
...baseTable,
Expand Down
40 changes: 22 additions & 18 deletions cli/api/utils/postgres.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,18 +65,32 @@ export class PgPoolExecutor {
}) => Promise<T>
) {
const client = await this.pool.connect();
// The client can be released from several places: the client "error" handler,
// the query "error" handler (which passes the error to release() so the bad
// connection is closed, not returned to the pool), and the finally block.
// Release exactly once -- otherwise pg-pool's throwOnDoubleRelease fires and a
// confusing "Release called on client which has already been released" surfaces
// ahead of the real error on every failing statement.
let released = false;
const releaseOnce = (err?: Error) => {
if (released) {
return;
}
released = true;
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing pg.Client", e.message, e.stack);
}
};
try {
client.on("error", err => {
// tslint:disable-next-line: no-console
console.error("pg.Client client error", err.message, err.stack);
// Errored connections cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Client", e.message, e.stack);
}
releaseOnce(err);
});

return await callback({
Expand DownExpand Up@@ -114,12 +128,7 @@ export class PgPoolExecutor {
// Errors don't cause "end" to fire, additionally errored connections
// cause issues when released back to the pool. Instead, close the connection
// by passing the error to release(). https://github.com/dataform-co/dataform/issues/914
try {
client.release(err);
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing errored pg.Query", e.message, e.stack);
}
releaseOnce(err);
reject(err);
});
query.on("end", () => {
Expand All@@ -129,12 +138,7 @@ export class PgPoolExecutor {
}
});
} finally {
try {
client.release();
} catch (e) {
// tslint:disable-next-line: no-console
console.error("Error thrown when releasing ended pg.Client", e.message, e.stack);
}
releaseOnce();
}
}

Expand Down
50 changes: 50 additions & 0 deletions tests/integration/postgres.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,56 @@ suite("@sqlanvil/integration/postgres", { parallel: true }, ({ before, after })
);
});

test("a failing statement rejects with the real error and no double-release noise", async () => {
// Capture console.error so we can assert the pg client isn't released twice
// (which would print "Release called on client which has already been released").
// tslint:disable-next-line: no-console
const originalError = console.error;
const logged: string[] = [];
// tslint:disable-next-line: no-console
console.error = (...args: any[]) => logged.push(args.join(" "));
let err: Error | undefined;
try {
await dbadapter.execute("selct 1");
} catch (e) {
err = e;
} finally {
// tslint:disable-next-line: no-console
console.error = originalError;
}
expect(err, "a bad statement should reject").to.be.an("error");
expect(err.message.toLowerCase()).to.contain("syntax error");
expect(
logged.join("\n"),
"the pg client must be released exactly once"
).to.not.match(/already been released/i);
});

test("a table with an unnamed postgres index creates (derived index name)", async () => {
const schema = "sa_integration_test_unnamed_index";
try {
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
} catch (e) {
// ignore
}
await dbadapter.execute(`create schema "${schema}"`);
const table: sqlanvil.ITable = {
enumType: sqlanvil.TableType.TABLE,
target: { schema, name: "with_idx" },
query: "select 1 as id",
postgres: { indexes: [{ columns: ["id"], unique: true }] }
};
const adapter = new ExecutionSql({ warehouse: "postgres" }, "1.4.8");
for (const task of adapter.publishTasks(table, { fullRefresh: true }).build()) {
await dbadapter.execute(task.statement);
}
const { rows } = await dbadapter.execute(
`select indexname from pg_indexes where schemaname = '${schema}' and tablename = 'with_idx'`
);
expect(rows.map((r: any) => r.indexname)).to.include("with_idx_id_key");
await dbadapter.execute(`drop schema if exists "${schema}" cascade`);
});

test("run", { timeout: 60000 }, async () => {
const compiledGraph = await compile("tests/integration/postgres_project", "project_e2e");

Expand Down
2 changes: 1 addition & 1 deletion version.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@
# SemVer line). DF_VERSION is the upstream dataform-co/dataform release this fork
# is synced to — surfaced as metadata (e.g. `sqlanvil --version`), not the package
# version. Bump SQLANVIL_VERSION for sqlanvil releases; bump DF_VERSION on upstream syncs.
SQLANVIL_VERSION = "1.4.0"
SQLANVIL_VERSION = "1.4.1"
DF_VERSION = "3.0.59"