annotato 0.1.16 → 0.2.1

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: 53d8c10c79ec7a147fde40ed706fa11b77e62a65f1afbb87a204b8f2153d6f29
4
- data.tar.gz: 488da6fb4970e359d266cc6e817502a72cdfc29a2d7ab59f10219c1e73978ef6
3
+ metadata.gz: ad60c697f9ee0213951d02981190dee26a8db0a2e48ab36d3118c1aaf95d4d40
4
+ data.tar.gz: 43a5f89b6509a636d579416c92bbbc52c63e15b8029701b46c525bd651a049a1
5
5
  SHA512:
6
- metadata.gz: 6dd785f4e27b78b9a2084d2f846e531aba53774be945e5534386a179165c9509028bb8010f97fbab57c31693ed33aeda2fc2ec7f3ee807b711f7ad4086270048
7
- data.tar.gz: 52d70e99d63296a518007970d09f20c209b15b5bb993b1c4f114c0912ba143c28d79e091491b74c7585898fef9c0c1140486db61df1a9782deefe935849720aa
6
+ metadata.gz: 7753fdc81bfc3568053c99f3f4f98107d0ca3b2ad1b64b127e19902e00f35a8b889b86b2219a431ea2dd673bab86000d0705b59d7a88ef1c68192be2cf012d3e
7
+ data.tar.gz: d05dd1bb4dd969a77555c3685ab129a9f31a479c5d2f69ef80c9ed7971677c044bc918ec2fd7e1212bc90eb49e678acd306f8afe66d7c9b743ea3b9dc3b9e650
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- annotato (0.1.16)
4
+ annotato (0.2.1)
5
5
  rails (>= 6.0)
6
6
 
7
7
  GEM
@@ -214,6 +214,7 @@ GEM
214
214
 
215
215
  PLATFORMS
216
216
  arm64-darwin-24
217
+ arm64-darwin-25
217
218
  x86_64-linux
218
219
 
219
220
  DEPENDENCIES
@@ -22,7 +22,7 @@ module Annotato
22
22
  lines << "Columns:"
23
23
  lines += ColumnFormatter.format(model, conn)
24
24
 
25
- enums = EnumFormatter.format(model)
25
+ enums = EnumFormatter.format(conn, conn.columns(table_name))
26
26
  unless enums.empty?
27
27
  lines << ""
28
28
  lines << "Enums:"
@@ -9,9 +9,11 @@ module Annotato
9
9
  table_name = model.table_name
10
10
  primary_key = model.primary_key
11
11
  unique_indexes = connection.indexes(table_name).select(&:unique)
12
- enums = model.defined_enums
13
12
  columns = connection.columns(table_name)
14
13
 
14
+ # Collect native PG enum type names once up front (avoids N+1 queries).
15
+ pg_enum_types = pg_enum_type_names(connection)
16
+
15
17
  # Compute max widths for name and sql_type so that the table lines up.
16
18
  name_width = columns.map(&:name).map(&:length).max
17
19
  type_width = columns.map(&:sql_type).map(&:length).max
@@ -20,8 +22,6 @@ module Annotato
20
22
  name = col.name
21
23
  type = col.sql_type
22
24
 
23
- # require "pry" if name == "allowed_statuses"
24
- # binding.pry if name == "allowed_statuses"
25
25
  # Build the left-hand side and calculate indent.
26
26
  left = "# %-#{name_width}s :%-#{type_width}s" % [name, type]
27
27
  # " default(" is 9 chars; +2 gives the extra gap.
@@ -41,7 +41,8 @@ module Annotato
41
41
  opts << "primary key" if name == primary_key
42
42
  opts << "is an Array" if type.end_with?("[]")
43
43
  opts << "unique" if unique_indexes.any? { |idx| idx.columns == [name] }
44
- opts << "enum" if enums.key?(name)
44
+ opts << "enum" if pg_enum_types.include?(type.delete_suffix("[]"))
45
+ opts << "comment: #{col.comment.inspect}" if col.respond_to?(:comment) && col.comment && !col.comment.empty?
45
46
 
46
47
  # Emit either a multiline block or a single line.
47
48
  if default_block
@@ -64,9 +65,8 @@ module Annotato
64
65
  end
65
66
  end
66
67
 
67
- # Returns nil (no default) or an Array of un-indented lines:
68
- # ["[", "\"A\",", "\"B\"", "]"] or ["{", "\"k\":v,", ... , "}"]
69
- # If the value is empty array or hash, returns ["[]"] or ["{}"].
68
+ # Returns nil for non-JSON/empty values, or an Array of un-indented inner lines
69
+ # for multiline formatting: e.g. ["\"A\",", "\"B\""] for a JSON array.
70
70
  def self.build_default_block(value)
71
71
  return nil if value.nil? || !value.is_a?(String)
72
72
  s = value.strip
@@ -76,15 +76,15 @@ module Annotato
76
76
  return nil unless parsed.is_a?(Array) || parsed.is_a?(Hash)
77
77
 
78
78
  if parsed.is_a?(Array)
79
- return if parsed.empty? # empty array → ["[]"]
79
+ return nil if parsed.empty?
80
80
 
81
- # Only a JSON array of strings?
81
+ # JSON array of strings
82
82
  parsed.map.with_index do |e, i|
83
83
  comma = i == parsed.size - 1 ? "" : ","
84
84
  %Q{"#{e}"#{comma}}
85
85
  end
86
86
  else
87
- return if parsed.empty? # empty hash → ["{}"]
87
+ return nil if parsed.empty?
88
88
 
89
89
  # JSON hash → key/value pairs
90
90
  parsed.map.with_index do |(k, v), i|
@@ -93,5 +93,16 @@ module Annotato
93
93
  end
94
94
  end
95
95
  end
96
+
97
+ # Fetches all native PostgreSQL enum type names in one query.
98
+ # Returns a Set for O(1) membership checks.
99
+ def self.pg_enum_type_names(connection)
100
+ rows = connection.exec_query(
101
+ "SELECT typname FROM pg_type WHERE typtype = 'e'",
102
+ "SQL"
103
+ )
104
+ rows.map { |r| r["typname"] }.to_set
105
+ end
106
+ private_class_method :pg_enum_type_names
96
107
  end
97
108
  end
@@ -1,26 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Annotato
4
+ # Formats PostgreSQL native enum type definitions for columns whose sql_type
5
+ # refers to a custom DB enum (e.g. `access_link_category`). Queries pg_enum
6
+ # directly so the output reflects the actual database definition, not the
7
+ # ActiveRecord-level enum mapping already visible in the model source.
4
8
  class EnumFormatter
5
- def self.format(model)
6
- return [] if model.defined_enums.empty?
9
+ def self.format(conn, columns)
10
+ # Collect columns backed by a native PG enum type.
11
+ # A native enum type is one that exists in pg_type with typtype = 'e'.
12
+ enum_columns = columns.select { |col| pg_enum_type?(conn, col.sql_type) }
13
+ return [] if enum_columns.empty?
7
14
 
8
- model.defined_enums.map do |attr, values|
9
- lines = ["# #{attr}: {"]
10
-
11
- formatted_values = values.map do |key, val|
12
- if key.to_s == val.to_s
13
- "#{key}"
14
- else
15
- "#{key} (#{val})"
16
- end
17
- end
18
-
19
- lines += formatted_values.map { |v| "# #{v}," }
20
- lines[-1] = lines[-1].chomp(',') # remove trailing comma from last line
21
- lines << "# }"
15
+ enum_columns.map do |col|
16
+ labels = pg_enum_labels(conn, col.sql_type)
17
+ lines = ["# #{col.name} (#{col.sql_type}): ["]
18
+ lines += labels.map.with_index { |label, i|
19
+ comma = i == labels.size - 1 ? "" : ","
20
+ "# #{label}#{comma}"
21
+ }
22
+ lines << "# ]"
22
23
  lines.join("\n")
23
24
  end
24
25
  end
26
+
27
+ private_class_method def self.pg_enum_type?(conn, sql_type)
28
+ # Strip array suffix (e.g. "my_enum[]") before checking.
29
+ type_name = sql_type.delete_suffix("[]")
30
+ result = conn.exec_query(
31
+ "SELECT 1 FROM pg_type WHERE typname = $1 AND typtype = 'e' LIMIT 1",
32
+ "SQL",
33
+ [type_name]
34
+ )
35
+ result.any?
36
+ end
37
+
38
+ private_class_method def self.pg_enum_labels(conn, sql_type)
39
+ type_name = sql_type.delete_suffix("[]")
40
+ conn.exec_query(
41
+ "SELECT e.enumlabel FROM pg_enum e " \
42
+ "JOIN pg_type t ON e.enumtypid = t.oid " \
43
+ "WHERE t.typname = $1 " \
44
+ "ORDER BY e.enumsortorder",
45
+ "SQL",
46
+ [type_name]
47
+ ).map { |r| r["enumlabel"] }
48
+ end
25
49
  end
26
50
  end
@@ -19,7 +19,6 @@ module Annotato
19
19
  annotation = AnnotationBuilder.build(model)
20
20
  write_annotation(model, annotation)
21
21
  end
22
- @output.puts "✅ Annotato completed"
23
22
  end
24
23
 
25
24
  private
@@ -46,7 +45,13 @@ module Annotato
46
45
  file = model_file(model)
47
46
  return unless file && File.exist?(file)
48
47
 
49
- content = File.read(file).dup # <--- duplicate string to avoid FrozenError
48
+ content = File.read(file).dup
49
+
50
+ # Verify the class definition exists before doing anything else.
51
+ unless content.match?(/class\s+\w+/)
52
+ @output.puts "⚠️ Skipped #{model.name} — class not found in file"
53
+ return
54
+ end
50
55
 
51
56
  # Extract the old Annotato annotation block if it exists
52
57
  old_annotation = content[/^# == Annotato Schema Info.*?(?=^class|\z)/m]
@@ -57,17 +62,11 @@ module Annotato
57
62
  return
58
63
  end
59
64
 
60
- # Remove old Annotato blocks
65
+ # Remove old Annotato blocks and legacy annotate-gem blocks
61
66
  content.gsub!(/^# == Annotato Schema Info.*?(?=^class|\z)/m, "")
62
67
  content.gsub!(/^# == Schema Information.*?(?=^class|\z)/m, "")
63
68
  content.rstrip!
64
69
 
65
- # Verify the class definition exists before appending annotation
66
- unless content.match?(/class\s+\w+/)
67
- @output.puts "⚠️ Skipped #{model.name} — class not found in file"
68
- return
69
- end
70
-
71
70
  # Append the new annotation at the end of the file
72
71
  content += "\n\n#{annotation}\n"
73
72
  File.write(file, content)
@@ -3,11 +3,11 @@
3
3
  module Annotato
4
4
  class TriggerFormatter
5
5
  def self.format(conn, table_name)
6
- conn.exec_query(<<~SQL).map { |r| "# #{r['tgname']}" }
7
- SELECT tgname FROM pg_trigger
8
- WHERE tgrelid = '#{table_name}'::regclass
9
- AND NOT tgisinternal;
10
- SQL
6
+ conn.exec_query(
7
+ "SELECT tgname FROM pg_trigger WHERE tgrelid = $1::regclass AND NOT tgisinternal",
8
+ "SQL",
9
+ [table_name]
10
+ ).map { |r| "# #{r['tgname']}" }
11
11
  end
12
12
  end
13
13
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Annotato
4
- VERSION = "0.1.16"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -19,5 +19,7 @@ namespace :annotato do
19
19
  # No MODEL passed → annotate all models
20
20
  annotator.run
21
21
  end
22
+
23
+ puts "✅ Annotato completed"
22
24
  end
23
25
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: annotato
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.16
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Serhii Bodnaruk
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-01-23 00:00:00.000000000 Z
10
+ date: 2026-06-30 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails