Repository navigation
Conversation
An application that named a schema through databaseSchema but did not also write a databaseInit hook split in two: the drift commands read the named schema while the DDL and the migration ledger landed in public, so sync applied cleanly and status reported the whole model missing forever. databaseSchema now lives in DbInit, where the Database is built, and the pool runs one hook composed of the schema step and the application's own databaseInit. The schema step reads pg_namespace, creates the schema only when it is missing, so a role without CREATE on the database still connects, and sets search_path to the quoted name. The public default runs nothing. Composition, not a default that overriders chain with super, keeps the split from coming back for an application that overrides databaseInit for something else. The examples, the Tour and the docs drop the duplicated hook and stop saying both halves are needed. Closes #315 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The page warned that the schema alone is on the path and that objects in public are not, but never said what to do about it. One sentence now points at databaseInit, which runs after the schema step. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
An application that overrides
databaseSchemawithout also writing adatabaseInitthat setssearch_pathsplits in two: every catalog read (status, sync, migrate, the drift gate, deploy check) looks in the named schema, while the DDL and the migration ledger land inpublic.sync --applyexits 0, thenstatusreports the whole model missing and exits 1, forever, with no warning. Every example carried the same eight line hook to dodge this, and the docs taught that both halves were needed.What
Naming the schema is now enough.
databaseSchemamoved fromDbAppintoDbInit, where theDatabaseis built, with the same name and the samepublicdefault, so existing overrides compile unchanged. The pool now runs one composed hook:trait DbInit { + def databaseSchema: String = "public" // moved here from DbApp def databaseInit: Connection -> Unit = _ => () // default still a no op def database = Database.connect(url, user, password, - databaseInit, + DbInit.connectionInit(databaseSchema, databaseInit), poolSize, acquireTimeout) }Observable behaviour:
databaseSchema = "public"(the default): nothing runs on the connection, Postgres's ownsearch_pathstays.search_pathto that schema alone, then runs the application'sdatabaseInit.databaseInit(forapplication_name, say) needs nosupercall to stay in its schema.databaseSchemabut on purpose never setsearch_path(catalog reads only) now has its connections switch to that schema.The four examples (blog, shop, todo, reminders), the blog and shop
CreateUsertools, the shopApiSuiteand blogPostOwnershipSuitetest apps, and the Tour drop their schema hook. The how to and reference pages describedatabaseSchemaalone.How
DbInit.connectionInitcomposes the schema step with the user's hook:Alternatives it beat:
databaseInitdefault that overriders chain withsuper.databaseInit(c). Rejected because any override that forgetssuperbrings the same silent split back, which is the trap this issue removes. The issue's "databaseSchema becomes the one override" is met through composition, not through a derived default.create schema if not existson every connection. Rejected because Postgres checks CREATE on the database before honouring IF NOT EXISTS, so a least privileged role that owns an existing schema would fail every pooled connection (the deployed shop is such a case). The cost is one catalog read per physical connection.How to review
modules/db/src/main/scala/io/eezo/db/DbInit.scala: the heart. CheckconnectionInitorder (schema step, then user hook), that thepublicbranch returnsinituntouched, that the closure captures only theschemaparameter and notthis(capture checking, no suppression), and theCreatedMeanwhilecatch: only 23505 and 42P06 are swallowed, anything else fails the connection.modules/db/src/main/scala/io/eezo/db/schema/Ddl.scala:qwidened fromprivatetoprivate[db]so DbInit reuses it. No public signature changes.modules/db/src/main/scala/io/eezo/db/DbApp.scala:databaseSchemaremoved here and inherited from DbInit, along with its stale comment aboutsearch_pathbeing set indatabaseInit.modules/db/src/test/scala/io/eezo/db/DbInitSuite.scala: start from the double quote test, which pins quoting on the generated statements through a recording fake JDBC connection, then the race and refusal tests.modules/db/src/test/scala/io/eezo/db/DbAppSuite.scala: the three Postgres cases. CheckinFreshSchemadrops the schema before and after, and the least privileged role case drops its role.modules/db/src/test/scala/io/eezo/db/support/Pg.scala: scaladoc only;Pg.searchPathstill isolates DbSuite and PgSuite.modules/example/src/test/scala/example/Tour.scala: the hook is replaced byoverride def databaseSchema = Schema0, which also fixes the Tour's own commands readingpublic.Main.scala,CreateUser.scalaand test apps: the hook and thejava.sql.Connectionimport are gone, nothing else.docs/how-to/configure-the-database.md,docs/reference/configuration.md, and the blog, todo and reminders READMEs: no page still says both halves are required. The todo README diff is mostly its old em dashes rewritten, which the per file prose check forced once the file was touched.The author must confirm:
databaseSchemaalone is enough: taken, as the issue recommends.DbInit.connectionInit; an override never needssuper, and the user's hook runs second so it can still setsearch_pathon purpose.databaseSchemalives: moved intoDbInit, same name, samepublicdefault;DbApp'sliveread andEezoApp's DriftGate call untouched.publicdefault: no statement at all, by exact string compare.search_pathis: the application's schema alone,publicnot appended; extensions' objects inpublicare off the path for any application that names a schema, and the how to says so.pg_namespacefirst, IF NOT EXISTS kept, plus the race catch for two connections that both found it missing.Ddl.qhelper (doubles embedded double quotes), not raw interpolation.CreatedMeanwhile) and its two DbInitSuite tests are in the diff but not in the implementer's report; check they match your reading of the risk.Ddl.q, widened fromprivatetoprivate[db], not a newDbInit.identifier. This touchesDdl.scala, which the contract lists as unchanged in signature; the change is visibility only, inside thedbpackage.How it was tested
Added, each seen failing before the change it covers:
DbAppSuite"databaseSchema alone puts the tables in that schema, creating it, and status agrees": aDbAppoverLibrarynaming a schema that does not exist yet, no hook;sync --applyexits 0,authoris only in that schema perinformation_schema,statusexits 0.DbAppSuite"a databaseInit of the application's own, without super, keeps the tables in databaseSchema": the same with anapplication_namehook that never callssuper.DbAppSuite"a role that owns its schema but cannot create one on the database still connects": pins the catalog check against the CREATE privilege trap.DbInitSuite"the default public schema runs nothing on the connection": zero calls on the connection.DbInitSuite"a named schema is created when missing, then becomes the search_path, before the hook": statement order.DbInitSuite"a schema that exists is not created again": onlyset search_pathruns.DbInitSuite"losing the race to create the schema to another connection still enters it, either SQLState": 23505 and 42P06.DbInitSuite"any other failure to create the schema fails the connection": 42501 propagates.DbInitSuite"a double quote in the schema name is doubled, so the name stays one identifier".Existing suites relying on the default
search_path(DbSuite and PgSuite throughPg.searchPath, CommandsSuite) pass unchanged. The Tour runs to the end withTOUR_NOPAUSE=1.bin/gateon the committed tree: GATE GREEN, db 320 passed, examples blog 13 and shop 11 passed, site 49 passed, every other module green.bin/prose-checkis clean on every changed file.Review status
The review loop converged: round 1 returned verdict clean with no findings.
Closes #315
🤖 Generated with Claude Code