session_settings.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. package executor
  2. import (
  3. "strings"
  4. "github.com/danfragoso/pizzasql-next/pkg/parser"
  5. "github.com/danfragoso/pizzasql-next/pkg/pgerror"
  6. "github.com/danfragoso/pizzasql-next/pkg/version"
  7. )
  8. // Session settings.
  9. //
  10. // Drivers and ORMs open a connection with SET statements for parameters that
  11. // have no PizzaSQL equivalent (client_encoding, extra_float_digits,
  12. // statement_timeout, ...). Rejecting them makes an unmodified client unusable,
  13. // so the engine acknowledges every SET and retains only search_path, which
  14. // current_schema() and SHOW report. The engine has no other configurable
  15. // settings; SHOW returns honest constants for the values it exposes.
  16. // normalizeSettingName lowercases a setting name and collapses whitespace so
  17. // SET TIME ZONE and SHOW time zone agree.
  18. func normalizeSettingName(name string) string {
  19. return strings.ToLower(strings.Join(strings.Fields(name), " "))
  20. }
  21. // effectiveSearchPath is the search path the session currently observes: SET
  22. // LOCAL while a transaction is open, otherwise the session value.
  23. func (e *Executor) effectiveSearchPath() []string {
  24. if len(e.localSearchPath) > 0 {
  25. return e.localSearchPath
  26. }
  27. if len(e.searchPath) == 0 {
  28. return []string{publicSchemaName}
  29. }
  30. return e.searchPath
  31. }
  32. // beginSettingScope records the session search path so ROLLBACK can restore it
  33. // and clears any SET LOCAL left by a previous transaction.
  34. func (e *Executor) beginSettingScope() {
  35. e.txSearchPath = e.searchPath
  36. e.localSearchPath = nil
  37. }
  38. // commitSettingScope ends a transaction: SET LOCAL expires and a session SET
  39. // stays in effect.
  40. func (e *Executor) commitSettingScope() {
  41. e.localSearchPath = nil
  42. e.txSearchPath = nil
  43. e.pgCatalog = nil
  44. }
  45. // rollbackSettingScope restores the session search path captured at BEGIN, so a
  46. // session SET inside an aborted transaction has no lasting effect.
  47. func (e *Executor) rollbackSettingScope() {
  48. if e.txSearchPath != nil {
  49. e.searchPath = e.txSearchPath
  50. }
  51. e.localSearchPath = nil
  52. e.txSearchPath = nil
  53. e.pgCatalog = nil
  54. }
  55. // executeSet acknowledges a session setting.
  56. func (e *Executor) executeSet(stmt *parser.SetStmt) (*Result, error) {
  57. name := normalizeSettingName(stmt.Name)
  58. if name == "search_path" {
  59. var entries []string
  60. for _, value := range stmt.Values {
  61. for _, part := range strings.Split(value, ",") {
  62. part = strings.Trim(strings.TrimSpace(part), `"`)
  63. if part != "" {
  64. entries = append(entries, part)
  65. }
  66. }
  67. }
  68. // Only the emulated schemas exist; drop anything else so SHOW and
  69. // current_schema() describe a schema the engine can actually resolve.
  70. known := make([]string, 0, len(entries))
  71. for _, entry := range entries {
  72. switch strings.ToLower(entry) {
  73. case publicSchemaName, pgCatalogSchemaName, informationSchemaName:
  74. known = append(known, strings.ToLower(entry))
  75. }
  76. }
  77. if len(known) == 0 {
  78. known = []string{publicSchemaName}
  79. }
  80. switch {
  81. case stmt.Local:
  82. // SET LOCAL is transaction-scoped: it has no effect outside a
  83. // transaction block and expires at COMMIT or ROLLBACK.
  84. if e.inTransaction {
  85. e.localSearchPath = known
  86. }
  87. default:
  88. // A session SET inside a transaction is undone by ROLLBACK, so
  89. // BEGIN keeps the previous value.
  90. e.searchPath = known
  91. e.localSearchPath = nil
  92. }
  93. // A different search path changes which schema is visible; drop the
  94. // statement-scoped catalog snapshot so the next read re-evaluates it.
  95. e.pgCatalog = nil
  96. }
  97. return NewResult("SET"), nil
  98. }
  99. // executeShow returns the value of a setting. Values that the engine does not
  100. // model are reported as constants that describe the actual behavior, and
  101. // unknown names fail rather than inventing a value.
  102. func (e *Executor) executeShow(stmt *parser.ShowStmt) (*Result, error) {
  103. name := normalizeSettingName(stmt.Name)
  104. value, err := e.settingValue(name)
  105. if err != nil {
  106. return nil, err
  107. }
  108. result := NewResult("SHOW")
  109. result.AddColumnWithType(name, "TEXT")
  110. result.AddRow(value)
  111. return result, nil
  112. }
  113. // settingValue reports the value of a setting exposed by SHOW and
  114. // current_setting(). Unknown names are rejected rather than invented.
  115. func (e *Executor) settingValue(name string) (string, error) {
  116. name = normalizeSettingName(name)
  117. var value string
  118. switch name {
  119. case "search_path":
  120. value = strings.Join(e.effectiveSearchPath(), ", ")
  121. case "server_version":
  122. value = version.PostgreSQLCompatVersion
  123. case "server_version_num":
  124. // PostgreSQL encodes major*10000 + minor*100.
  125. value = "140000"
  126. case "server_encoding", "client_encoding":
  127. value = "UTF8"
  128. case "transaction_isolation", "default_transaction_isolation":
  129. value = "read committed"
  130. case "standard_conforming_strings", "integer_datetimes":
  131. value = "on"
  132. case "datestyle":
  133. value = "ISO, MDY"
  134. case "timezone", "time zone":
  135. value = "UTC"
  136. case "application_name":
  137. value = ""
  138. case "is_superuser":
  139. value = "off"
  140. default:
  141. return "", pgerror.Unsupported("unrecognized configuration parameter %q", name)
  142. }
  143. return value, nil
  144. }