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鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding support for pg-native
#4327
Conversation
Noting Node 10 integration build failed due to pull request rate limit being hit. Hopefully it should pass after rate limits let off |
I would rather see this implemented as a separate npm module (in contributing.md there are instructions how to make dialects that are not in knex core), which contains also better tests checking that it works. Otherwise this will start burdening knex more with bug reports, when people start using it and thinks that everything will work the same between pg / pg-native variants. That repository then will have some other people maintaining it and will be able to fix pg-native related issues of their side. Also I haven't seen the actual problem anywhere stated what is your use case and exact problem where would you need to use pg-native instead of pg. That difference between parsing @brianc has been really active in removing performance differences between pg-native and pg versions and most probably this will end up deprecated pretty soon. If need for pg-native does get lots of momentum and pg performance issues are impossible to fix, I would consider again if it is a good idea to have this after all in knex core and support it officially and include complete integrations test suites to be ran with it from knex core side. |
Thanks for the quick response! If we profile the standard When compared to our older service using a different stack to the same database (we are migrating away from this stack to the new stack with When high quantities of writes happen concurrently that is when we see this issue crop up most notably. |
I do acknowledge the original issue and see why you like to use the pg-native. Though a clear testcase which demonstrates the problem (I know that brianc was able to somewhat demonstrate it by himself according to the linked issue) would probably be really useful. Even some docker based stress test example. Though all the above is mostly irrelevant since the main point of my last comment was that: Is there any reason why wouldn't you do this in a separate npm module and provide maintenance for it? It can be even published under knex organization in github if you wish. This will start burdening knex maintainers in case if it is added to core and I don't believe that need for this is going to remain forever, which would also make it more suitable to reside in a separate npm package with own maintainers. |
To be fair, we are not getting additional burden due to supporting If |
Thanks for the guidance! Yep, I can totally understand and appreciate the maintenance burden. A module would likely be sufficient - we've been running from a fork. I can look into it more today. If the decision goes the direction of favoring not having this in core we can go that direction. I think some of the changes may want to be considered regardless if I have read the client.connect(callback: (err: Error) => void) => void and this is also the same in Since the default client.connect(function (err, connection) looks to be on a deprecated callback signature. Removing const clientConnection = new client.driver.Client(client.connectionSettings); seems like it would match their newest guidelines. I could 100% be reading some of this wrong, but that's what it seemed like to me on my initial digging. From there I figured it was a small leap to load in the native driver since the major blocker (connection syntax) was removed. |
I actually have resolved for example pooling issues, where mysql2 was a bit unstable not recognizing broken connections and probably with pg most of the open issues will be about streaming support and I don't think that mysql2 version should be supported be us either :) When things are fixed in pg diealect, they still would be automatically fixed to external Biggest thing with adding this to knex will be probably to add it to knex's integration tests to make sure that current cases (except for streaming) works as expected (no need to check query sql generation). |
@elhigu Would it be acceptable for me to reduce my PR down to just adjusting the callback signature + remove the This also adds the benefit of thinking through first-class support at a different time. Demonstration for same behavior: https://github.com/brianc/node-postgres/blob/25f658f227a1bcbe759423678a7ab4ba8e067994/packages/pg/lib/client.js#L279 |
To me either way is the same. Both will add more features to knex core, when I would like to push knex to have more modular design with less features in core package and more pushed to additional modules. If the community wants it in anyways, I suppose having the configure flag for it or implementing it as a completely separate dialect like mysql2 is better. |
@elhigu Got it. So what are next steps? Should this PR be closed? Should we wait for additional community input? |
@machuga I would say that if you can rework the PR in a way that follows the |
Thanks @kibertoad! I'll get started down that path soon. |
Alright the code has been updated to use a separate driver. I'm still looking around a bit to see if I missed any other places this code should be referenced, but I think it is fairly complete. |
@machuga https://github.com/knex/knex/blob/master/.github/workflows/coverage.yml also needs to be updated. |
Also documentation at https://github.com/knex/documentation needs to be updated |
Also there is a bunch of |
Thanks for the guidance! You've already seen it, but for others: I have separated this out to #4356 to keep the extra helpers clean up separate from the |
@kibertoad Got it! I'll get these updated. I noticed that |
Should be able to come back to this this weekend. Thanks for your patience! |
Going back to some of the earlier comments, the streaming may not work, but I am looking into it. |
@machuga Can I help you with finishing this? |
Hi @kibertoad! Sure. I think I'll have a few questions regarding which feature I should disallow and how to reject them according to conventions. Thanks for reminding me to get back on it. |
@kibertoad I had a design question pop up. The The |
@machuga I don't think we should be tinkering with driver output here, generally we prefer to stay out of driver's way. Case-insensitivity in tests sounds like a better approach. |
lib/dialects/pgnative/index.js
Outdated
|
||
class Client_PgNative extends Client_PG { | ||
_stream() { | ||
console.log('HERE WE GO'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will get rebased out after debugging complete.
@kibertoad since pg-native
is incompatible with pg-query-stream
I'll need to either:
- Disable support for streaming as something like the below line where an exception gets thrown at invocation (though this method isn't the correct place from what I can tell, perhaps something in the execution lib)
- Attempt to implement a stream that has the same feeling, but will not have the same response characteristics.
lib/dialects/pgnative/index.js
Outdated
_escapeBinding2 = makeEscape({ | ||
escapeArray(val, esc) { | ||
return esc(arrayString(val, esc)); | ||
}, | ||
escapeString(str) { | ||
let hasBackslash = false; | ||
let escaped = "'"; | ||
for (let i = 0; i < str.length; i++) { | ||
const c = str[i]; | ||
if (c === "'") { | ||
escaped += c + c; | ||
} else if (c === '\\') { | ||
escaped += c + c; | ||
hasBackslash = true; | ||
} else { | ||
escaped += c; | ||
} | ||
} | ||
escaped += "'"; | ||
if (hasBackslash === true) { | ||
escaped = 'E' + escaped; | ||
} | ||
return escaped; | ||
}, | ||
escapeObject(val, prepareValue, timezone, seen = []) { | ||
if (val && typeof val.toPostgres === 'function') { | ||
seen = seen || []; | ||
if (seen.indexOf(val) !== -1) { | ||
throw new Error( | ||
`circular reference detected while preparing "${val}" for query` | ||
); | ||
} | ||
seen.push(val); | ||
return prepareValue(val.toPostgres(prepareValue), seen); | ||
} | ||
return JSON.stringify(val); | ||
}, | ||
}); | ||
} | ||
|
||
Object.assign(Client_PgNative.prototype, { | ||
driverName: 'pgnative', | ||
canCancelQuery: true, | ||
_escapeBinding: makeEscape({ | ||
escapeArray(val, esc) { | ||
return esc(arrayString(val, esc)); | ||
}, | ||
escapeString(str) { | ||
let hasBackslash = false; | ||
let escaped = "'"; | ||
for (let i = 0; i < str.length; i++) { | ||
const c = str[i]; | ||
if (c === "'") { | ||
escaped += c + c; | ||
} else if (c === '\\') { | ||
escaped += c + c; | ||
hasBackslash = true; | ||
} else { | ||
escaped += c; | ||
} | ||
} | ||
escaped += "'"; | ||
if (hasBackslash === true) { | ||
escaped = 'E' + escaped; | ||
} | ||
return escaped; | ||
}, | ||
escapeObject(val, prepareValue, timezone, seen = []) { | ||
if (val && typeof val.toPostgres === 'function') { | ||
seen = seen || []; | ||
if (seen.indexOf(val) !== -1) { | ||
throw new Error( | ||
`circular reference detected while preparing "${val}" for query` | ||
); | ||
} | ||
seen.push(val); | ||
return prepareValue(val.toPostgres(prepareValue), seen); | ||
} | ||
return JSON.stringify(val); | ||
}, | ||
}), | ||
}); | ||
|
||
function arrayString(arr, esc) { | ||
let result = '{'; | ||
for (let i = 0; i < arr.length; i++) { | ||
if (i > 0) result += ','; | ||
const val = arr[i]; | ||
if (val === null || typeof val === 'undefined') { | ||
result += 'NULL'; | ||
} else if (Array.isArray(val)) { | ||
result += arrayString(val, esc); | ||
} else if (typeof val === 'number') { | ||
result += val; | ||
} else { | ||
result += JSON.stringify(typeof val === 'string' ? val : esc(val)); | ||
} | ||
} | ||
return result + '}'; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will rebase out and deduplicate between the two strategies next commit. Put here during experimentation.
@kibertoad I've opted for a faked-out stream (it will stream the results after they're fetched in order to keep the same interface as other drivers. I have four timeout failures happening right now with the pg-native driver that I need to debug. I'm not sure the timeout support is available in pg-native yet so I need to verify. |
Awesome, thank you for the update! |
Query cancellation in pgnative must be handled via the On clean runs this normally works, but there a few issues:
Trying to work around it and properly debug |
This enables pgnative to run in CI as well.
Using the newer syntax in the default postgres strategy and inheriting it into the pg-native. Also extracting connection creation in postgres dialect The `pg` library, both standard and `native` clients, offers the option to return a promise instead of callback when calling `connect`. The unit tests for `pg` were mocking out the `connect` call, but that relied on the deprecated call signature in `pg` standard client to work. The `_acquireOnlyConnection` call will now acquire the connection and connect to it without any of the additional configuration of the rest of `acquireRawConnection` to keep the encapsulation clean and make it easily mockable if necessary.
The stream is not a legit stream-as-you-go solution via following a cursor due to limitations of pg-native. Instead, to keep the interface consistent, this streams the results of the response after they're fetched.
@@ -62,34 +62,40 @@ class Client_PG extends Client { | |||
return `"${value.replace(/"/g, '""')}"${arrayAccessor}`; | |||
} | |||
|
|||
_acquireOnlyConnection() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was introduction of this method and modification of acquireRawConnection
necessary if pgnative doesn't override either?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This goes back to connect
using the deprecated (at least according to docs) callback signature where the same connection is returned: https://github.com/brianc/node-postgres/blob/master/packages/pg/lib/client.js#L279
The native client doesn't do this. The mocking was dependent on this behavior, which is now undocumented, so I moved it toward being compliant with the current node-postgres
documentation + compatible with pg-native
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/brianc/node-postgres/blob/master/packages/pg/lib/native/client.js#L119-L134 native version for comparison.
Our notoriously flaky test is failing, any chance you could take a look at it? |
Able to reproduce on my machine if I run it in isolation. Integration Tests
postgresql | pgnative
NOTICE: table "migration_test_1" does not exist, skipping
NOTICE: table "migration_test_2" does not exist, skipping
NOTICE: table "migration_test_2_1" does not exist, skipping
knex.migrate.latest in parallel
Error: create table "knex_migrations" ("id" serial primary key, "name" varchar(255), "batch" integer, "migration_time" timestamptz) - duplicate key value violates unique constraint "pg_type_typname_nsp_index"
at module.exports.Client._emitResult (./src/knex/node_modules/pg-native/index.js:173:26)
at module.exports.Client._read (./src/knex/node_modules/pg-native/index.js:215:31)
at PQ.emit (node:events:365:28) {
severity: 'ERROR',
code: '23505',
messageDetail: 'Key (typname, typnamespace)=(knex_migrations_id_seq, 2200) already exists.',
schema: 'pg_catalog',
table: 'pg_type',
constraint: 'pg_type_typname_nsp_index',
file: 'nbtinsert.c',
line: '649',
routine: '_bt_check_unique'
}
[
1,
[ '20131019235242_migration_1.js', '20131019235306_migration_2.js' ]
] This is what I get from the promise results in this area. Digging in a bit further. |
Moving to constructor assignment for driverName nad cancelability
Okay the error that came up was not the same, looked like it was something triggered by running in isolation only. Tests passing now 馃憤 |
@@ -31,6 +33,14 @@ services: | |||
- POSTGRES_PASSWORD=postgresrootpassword | |||
- POSTGRES_USER=postgres | |||
|
|||
pgnative: | |||
image: mdillon/postgis |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the advantage of using this image over vanilla pg?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not too sure if there are technical advantages, but the postgresql
strategy uses it above on line 27. I assumed there was a good reason for it. For benefit of pg-native
development, it makes debugging easier by ensuring there aren't any odd bugs because of configuration issues across two different images.
Thank you! It was an epic journey. |
Thanks for your continuous help and patience! It's been fun. 馃檶 |
Why is the dialect Also, |
Does this driver support CockroachDB? |
馃憢 Hello!
Thanks for all the work on this project, it has been a great help to our team.
Based on feedback from @tgriesser and #4062, I am submitting a small PR to allow for
pg-native
to be used.Problem
Due to problems with the non-native driver holding open connections longer than necessary, and those in this thread, my team and I need to adopt the
pg-native
library. In production the difference is very noticeable, where our pool gets drained much quicker than it should and starts causing issues at scale (connections timing out, locking issues, session issues in AWS RDS).Description
Based on feedback, I've created a new
pgnative
dialect that descends from thepostgresql
dialect, but loads in the native bindings and will use the updated callback signature forclient.connect
that is compatible with both thenode-postgres
andpg-native
bindings.Installing
pg-native
module in a project, then selecting this dialect will allow this strategy to work. It has the same configuration object as the normalpostgresql
dialect.Example Configuration
Old Description Before Feedback - Discard, kept for posterity.
This PR allows for the
pg-native
library to be utilized in place of the standardpg
bindings if either theNODE_PG_FORCE_NATIVE
env var is set, or if theuseNative
config is set totrue
when passed into the strategy.Example Configuration
Note
This is my first PR to this code base so I am assuming not everything is ready to go. I've added an integration test configuration in a way that may not be ideal - I'm happy to tweak it to fit the situation better.