pgdexter 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c68746f6134f6603c5549b561886b45ee3432df5d1e07e65a5907559901d333a
4
- data.tar.gz: 699c2744f3e2c9a79f8fa9ca6ae7a77586421243cbf7a1f92642f76724cc0cb0
3
+ metadata.gz: d40fb941685400a6a158502895da0d8b93d94db03da0ca4551714aea36e3dae3
4
+ data.tar.gz: 8b99de845f8d6f2e455e98c3505e689a0a55dac69f7c08ba6ebebabeeb1ddd63
5
5
  SHA512:
6
- metadata.gz: 343bc52539ef09541fd0774667034ea02fb88b0e4f820e52c86d77df46d059c253576c22272b91a407b764965f66c554278154da6ab923249ce9979ab3a5aed0
7
- data.tar.gz: a9b86931b4fc58d2b89c87f46f5008dfd47274cc39f096ec2b5143bea8858e29a786e387f9ccc29fe74475b5b0317e9d348d36f2a686cb19457ee79179a270f5
6
+ metadata.gz: b2b25419796e276074f5a49b2d109be72789ddf8a5c7c5cf29ba55900ac79c323e55b06bf5ab20dde4d34e4cf1e752e7362d23481d21d19a8b068a360562d13f
7
+ data.tar.gz: 382792e95461d718489d36c71e1ba01940f4b1d76de1b80527f0a4a19fcc90c215366645640970271011d5ad2c264051be57e62684e3278248d52bfaae983cd2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.5.3 (2024-03-05)
2
+
3
+ - Fixed error with hypothetical index limit
4
+ - Fixed error with foreign tables
5
+
6
+ ## 0.5.2 (2024-01-10)
7
+
8
+ - Added Docker image for `linux/arm64`
9
+ - Switched to `GENERIC_PLAN` for Postgres 16
10
+ - Fixed error with `auto_explain`
11
+ - Fixed warning with Ruby 3.3
12
+
1
13
  ## 0.5.1 (2023-05-27)
2
14
 
3
15
  - Fixed `JSON::NestingError`
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2017-2023 Andrew Kane
1
+ Copyright (c) 2017-2024 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -4,7 +4,7 @@ The automatic indexer for Postgres
4
4
 
5
5
  [Read about how it works](https://ankane.org/introducing-dexter) or [watch the talk](https://www.youtube.com/watch?v=Mni_1yTaNbE)
6
6
 
7
- [![Build Status](https://github.com/ankane/dexter/workflows/build/badge.svg?branch=master)](https://github.com/ankane/dexter/actions)
7
+ [![Build Status](https://github.com/ankane/dexter/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/dexter/actions)
8
8
 
9
9
  ## Installation
10
10
 
@@ -96,10 +96,16 @@ module Dexter
96
96
  analyze_tables(tables) if tables.any? && (@analyze || @log_level == "debug2")
97
97
 
98
98
  # create hypothetical indexes and explain queries
99
- candidates = tables.any? ? create_hypothetical_indexes(queries.select(&:candidate_tables)) : {}
99
+ if tables.any?
100
+ # process in batches to prevent "hypopg: not more oid available" error
101
+ # https://hypopg.readthedocs.io/en/rel1_stable/usage.html#configuration
102
+ queries.select(&:candidate_tables).each_slice(500) do |batch|
103
+ create_hypothetical_indexes(batch)
104
+ end
105
+ end
100
106
 
101
107
  # see if new indexes were used and meet bar
102
- new_indexes = determine_indexes(queries, candidates, tables)
108
+ new_indexes = determine_indexes(queries, tables)
103
109
 
104
110
  # display and create new indexes
105
111
  show_and_create_indexes(new_indexes, queries)
@@ -185,7 +191,7 @@ module Dexter
185
191
 
186
192
  # get initial costs for queries
187
193
  calculate_plan(queries)
188
- explainable_queries = queries.select { |q| q.explainable? && q.high_cost? }
194
+ explainable_queries = queries.select { |q| q.plans.any? && q.high_cost? }
189
195
 
190
196
  # filter tables for performance
191
197
  tables = Set.new(explainable_queries.flat_map(&:tables))
@@ -228,7 +234,9 @@ module Dexter
228
234
  calculate_plan(explainable_queries)
229
235
  end
230
236
 
231
- candidates
237
+ queries.each do |query|
238
+ query.candidates = candidates
239
+ end
232
240
  end
233
241
 
234
242
  def find_columns(plan)
@@ -282,9 +290,8 @@ module Dexter
282
290
  query_indexes
283
291
  end
284
292
 
285
- def determine_indexes(queries, candidates, tables)
293
+ def determine_indexes(queries, tables)
286
294
  new_indexes = {}
287
- index_name_to_columns = candidates.invert
288
295
 
289
296
  # filter out existing indexes
290
297
  # this must happen at end of process
@@ -313,11 +320,11 @@ module Dexter
313
320
  cost_savings2 = new_cost > 100 && new_cost2 < new_cost * savings_ratio
314
321
 
315
322
  key = cost_savings2 ? 2 : 1
316
- query_indexes = hypo_indexes_from_plan(index_name_to_columns, query.plans[key], index_set)
323
+ query_indexes = hypo_indexes_from_plan(query.candidates, query.plans[key], index_set)
317
324
 
318
325
  # likely a bad suggestion, so try single column
319
326
  if cost_savings2 && query_indexes.size > 1
320
- query_indexes = hypo_indexes_from_plan(index_name_to_columns, query.plans[1], index_set)
327
+ query_indexes = hypo_indexes_from_plan(query.candidates, query.plans[1], index_set)
321
328
  cost_savings2 = false
322
329
  end
323
330
 
@@ -390,8 +397,8 @@ module Dexter
390
397
 
391
398
  # TODO optimize
392
399
  if @log_level.start_with?("debug")
393
- query.pass1_indexes = hypo_indexes_from_plan(index_name_to_columns, query.plans[1], index_set)
394
- query.pass2_indexes = hypo_indexes_from_plan(index_name_to_columns, query.plans[2], index_set)
400
+ query.pass1_indexes = hypo_indexes_from_plan(query.candidates, query.plans[1], index_set)
401
+ query.pass2_indexes = hypo_indexes_from_plan(query.candidates, query.plans[2], index_set)
395
402
  end
396
403
  end
397
404
  end
@@ -521,8 +528,8 @@ module Dexter
521
528
  raise Dexter::Abort, e.message
522
529
  end
523
530
 
524
- def execute(query, pretty: true, params: [])
525
- # use exec_params instead of exec for security
531
+ def execute(query, pretty: true, params: [], use_exec: false)
532
+ # use exec_params instead of exec when possible for security
526
533
  #
527
534
  # Unlike PQexec, PQexecParams allows at most one SQL command in the given string.
528
535
  # (There can be semicolons in it, but not more than one nonempty command.)
@@ -533,7 +540,11 @@ module Dexter
533
540
  log colorize("[sql] #{query}#{params.any? ? " /*#{params.to_json}*/" : ""}", :cyan) if @log_sql
534
541
 
535
542
  @mutex.synchronize do
536
- conn.exec_params("#{query} /*dexter*/", params).to_a
543
+ if use_exec
544
+ conn.exec("#{query} /*dexter*/").to_a
545
+ else
546
+ conn.exec_params("#{query} /*dexter*/", params).to_a
547
+ end
537
548
  end
538
549
  end
539
550
 
@@ -543,7 +554,9 @@ module Dexter
543
554
 
544
555
  # try to EXPLAIN normalized queries
545
556
  # https://dev.to/yugabyte/explain-from-pgstatstatements-normalized-queries-how-to-always-get-the-generic-plan-in--5cfi
546
- explain_normalized = query.include?("$1")
557
+ normalized = query.include?("$1")
558
+ generic_plan = normalized && server_version_num >= 160000
559
+ explain_normalized = normalized && !generic_plan
547
560
  if explain_normalized
548
561
  prepared_name = "dexter_prepared"
549
562
  execute("PREPARE #{prepared_name} AS #{safe_statement(query)}", pretty: false)
@@ -566,12 +579,14 @@ module Dexter
566
579
  end
567
580
  end
568
581
 
582
+ explain_prefix = generic_plan ? "GENERIC_PLAN, " : ""
583
+
569
584
  # strip semi-colons as another measure of defense
570
- plan = JSON.parse(execute("EXPLAIN (FORMAT JSON) #{safe_statement(query)}", pretty: false).first["QUERY PLAN"], max_nesting: 1000).first["Plan"]
585
+ plan = JSON.parse(execute("EXPLAIN (#{explain_prefix}FORMAT JSON) #{safe_statement(query)}", pretty: false, use_exec: generic_plan).first["QUERY PLAN"], max_nesting: 1000).first["Plan"]
571
586
 
572
587
  if @log_explain
573
588
  # Pass format to prevent ANALYZE
574
- puts execute("EXPLAIN (FORMAT TEXT) #{safe_statement(query)}", pretty: false).map { |r| r["QUERY PLAN"] }.join("\n")
589
+ puts execute("EXPLAIN (#{explain_prefix}FORMAT TEXT) #{safe_statement(query)}", pretty: false, use_exec: generic_plan).map { |r| r["QUERY PLAN"] }.join("\n")
575
590
  end
576
591
 
577
592
  plan
@@ -587,7 +602,8 @@ module Dexter
587
602
  columns_by_table.each do |table, cols|
588
603
  # no reason to use btree index for json columns
589
604
  cols.reject { |c| ["json", "jsonb"].include?(c[:type]) }.permutation(n) do |col_set|
590
- candidates[col_set] = create_hypothetical_index(table, col_set)
605
+ index_name = create_hypothetical_index(table, col_set)
606
+ candidates[index_name] = col_set
591
607
  end
592
608
  end
593
609
  end
@@ -604,6 +620,7 @@ module Dexter
604
620
  information_schema.tables
605
621
  WHERE
606
622
  table_catalog = current_database()
623
+ AND table_type IN ('BASE TABLE', 'VIEW')
607
624
  SQL
608
625
  result.map { |r| r["table_name"] }
609
626
  end
data/lib/dexter/query.rb CHANGED
@@ -2,7 +2,7 @@ module Dexter
2
2
  class Query
3
3
  attr_reader :statement, :fingerprint, :plans
4
4
  attr_writer :tables
5
- attr_accessor :missing_tables, :new_cost, :total_time, :calls, :indexes, :suggest_index, :pass1_indexes, :pass2_indexes, :pass3_indexes, :candidate_tables, :tables_from_views
5
+ attr_accessor :missing_tables, :new_cost, :total_time, :calls, :indexes, :suggest_index, :pass1_indexes, :pass2_indexes, :pass3_indexes, :candidate_tables, :tables_from_views, :candidates
6
6
 
7
7
  def initialize(statement, fingerprint = nil)
8
8
  @statement = statement
@@ -31,7 +31,7 @@ module Dexter
31
31
  end
32
32
 
33
33
  def explainable?
34
- plans.any?
34
+ plans.size >= 3
35
35
  end
36
36
 
37
37
  def costs
@@ -1,3 +1,3 @@
1
1
  module Dexter
2
- VERSION = "0.5.1"
2
+ VERSION = "0.5.3"
3
3
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgdexter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-27 00:00:00.000000000 Z
11
+ date: 2024-03-06 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: csv
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: pg
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -96,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
110
  - !ruby/object:Gem::Version
97
111
  version: '0'
98
112
  requirements: []
99
- rubygems_version: 3.4.10
113
+ rubygems_version: 3.5.3
100
114
  signing_key:
101
115
  specification_version: 4
102
116
  summary: The automatic indexer for Postgres