tina4ruby 3.13.16 → 3.13.18

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: b1cf076eb39a57f9c1cbdff22ff957b6abf2bc24ab245d603a88f4fdc780ab1f
4
- data.tar.gz: 87a49969a5de07822dfeee8f4d6792dd0260409344519396e108ee45d8c68245
3
+ metadata.gz: 0073552dcac0e437fa36de2038f6958cbdbcde9077ec2427d9fd1bf77242da0e
4
+ data.tar.gz: c5c667382a67031bef1d7f3646020984942079edb5de9c5084c135ce4be68e9d
5
5
  SHA512:
6
- metadata.gz: a46003bec7bcaf5c5a4df7731a80b6d4e77b4eca9a9f467dcd061e94db7372192ba8751cf29d333518c49988224172adfab0a05bd16a14bc65fa2d2a5a209d98
7
- data.tar.gz: ff0760e9a50bfe4f07dafc1c842060693a710de0ff5c3c9c5adfe606e858aee4219ea4e5bbdda600cd0252f89f83f749b619e426a884f0a6260e0c5552ce04bf
6
+ metadata.gz: d7e9036bfe01826691e30382e35b8c4f70dfe2043602f41abae7a0e573993992a99f89e733b2b7dd74e4c2c2d6fe1c6be7faeb4d57d132d0be6c1d471f538afb
7
+ data.tar.gz: 7a4ec3bdd62b40c973e2fadbd52f205d92bc6fd2f0e18c1407a164a6cb27fcb03bffb50ec789851ac567105c1da81c6fbdcd00ece6fdbefef2411d1067f55610
@@ -25,6 +25,8 @@ module Tina4
25
25
  url = uri.to_s
26
26
  end
27
27
  @connection = PG.connect(url)
28
+ apply_result_type_map(@connection)
29
+ @connection
28
30
  end
29
31
 
30
32
  def close
@@ -151,6 +153,55 @@ module Tina4
151
153
 
152
154
  private
153
155
 
156
+ # Decode result columns to native Ruby types (parity with SQLite, and
157
+ # with Python psycopg2 / Node node-postgres which both return native
158
+ # types). Without this the pg gem hands back EVERY column as a String —
159
+ # ``id: "1"``, ``active: "t"``, timestamps as strings — so an app
160
+ # written on SQLite silently changes behaviour on PostgreSQL.
161
+ #
162
+ # PG::BasicTypeMapForResults decodes by column OID:
163
+ # int2/int4/int8/serial -> Integer
164
+ # bool -> true / false
165
+ # float4/float8 -> Float
166
+ # numeric -> BigDecimal
167
+ # timestamp/timestamptz -> Time
168
+ # date -> Date
169
+ # text/varchar -> String (unchanged)
170
+ #
171
+ # The map is set on the *connection*, so it applies uniformly to both
172
+ # ``exec`` and ``exec_params`` and therefore to every fetch / fetch_one /
173
+ # columns path that flows through execute_query.
174
+ #
175
+ # uuid / json / jsonb / regclass have no built-in coder; left alone the
176
+ # map prints a noisy "no type cast defined" warning to stderr and falls
177
+ # back to a raw string anyway. We register explicit text decoders for them
178
+ # so they stay plain strings (the documented behaviour) without the
179
+ # warning — regclass matters because table_exists? selects to_regclass().
180
+ # bytea is already handled by the map as an ASCII-8BIT binary string,
181
+ # which is what decode_blobs expects.
182
+ def apply_result_type_map(conn)
183
+ type_map = PG::BasicTypeMapForResults.new(conn)
184
+ register_text_decoders(conn, type_map, %w[uuid json jsonb regclass])
185
+ conn.type_map_for_results = type_map
186
+ rescue PG::Error, NameError
187
+ # If the type map can't be built (e.g. a minimal pg build without
188
+ # BasicTypeMapForResults, or a connection that can't resolve OIDs),
189
+ # leave results as strings rather than breaking the connection.
190
+ nil
191
+ end
192
+
193
+ # Register a plain-text decoder for each named PostgreSQL type so the
194
+ # result map returns it unchanged as a String instead of warning that no
195
+ # cast is defined. Unknown type names are skipped silently.
196
+ def register_text_decoders(conn, type_map, type_names)
197
+ type_names.each do |name|
198
+ oid = conn.exec_params("SELECT $1::regtype::oid", [name]).getvalue(0, 0).to_i
199
+ type_map.add_coder(PG::TextDecoder::String.new(oid: oid, name: name, format: 0))
200
+ rescue PG::Error
201
+ next
202
+ end
203
+ end
204
+
154
205
  def convert_placeholders(sql)
155
206
  counter = 0
156
207
  sql.gsub("?") { counter += 1; "$#{counter}" }
@@ -84,7 +84,12 @@ module Tina4
84
84
  # Automatically:
85
85
  # - Registers an integer field for the column
86
86
  # - Calls belongs_to on this class (strip _id suffix for association name)
87
- # - Calls has_many on the referenced class (if already loaded)
87
+ # - Calls has_many on the referenced class. The accessor name is the
88
+ # declaring class name lowercased + "s" (e.g. Post → posts), matching
89
+ # Python; override with related_name:. Works whether the referenced
90
+ # class loaded before OR after this one, including the string form
91
+ # (references: "Author") for forward references — resolution is
92
+ # deferred via Tina4::ORM.inherited until the target class exists.
88
93
  #
89
94
  # @param name [Symbol] Column name (e.g. :user_id)
90
95
  # @param references [Class, String] Referenced model class or its name
@@ -122,12 +127,45 @@ module Tina4
122
127
  has_many_name: hm_key.to_sym,
123
128
  foreign_key: name.to_s
124
129
  }
130
+
131
+ # If the referenced model is ALREADY loaded (string form whose target
132
+ # is defined, or any forward reference that has since resolved), wire
133
+ # the deferred entry right now. The inherited hook on Tina4::ORM only
134
+ # fires when a NEW class is defined, so a string reference to a class
135
+ # that loaded BEFORE this one would otherwise never wire its has_many
136
+ # side. resolve_referenced_class returns the live class for either the
137
+ # Class or the string form.
138
+ resolved = resolve_referenced_class(references)
139
+ resolved.apply_fk_registry! if resolved && resolved.respond_to?(:apply_fk_registry!, true)
140
+ end
141
+
142
+ # Resolve a ForeignKeyField `references:` argument to a live Tina4::ORM
143
+ # subclass, or nil if it is not (yet) loaded. Accepts either a Class or a
144
+ # class-name String (with or without a namespace). The string form is the
145
+ # documented deferred-safe path: a bare constant used before its class is
146
+ # defined raises NameError (plain Ruby), so forward references must be
147
+ # written as strings.
148
+ def resolve_referenced_class(references)
149
+ return references if references.is_a?(Class)
150
+ return nil unless defined?(Tina4::ORM) && Tina4::ORM.respond_to?(:model_subclasses)
151
+
152
+ simple = references.to_s.split("::").last
153
+ Tina4::ORM.model_subclasses.find do |klass|
154
+ klass.name && klass.name.split("::").last == simple
155
+ end
156
+ rescue StandardError
157
+ nil
125
158
  end
126
159
 
127
160
  # Apply any deferred FK-registry has_many wiring for this class.
128
161
  # Called automatically when a class that is referenced by a ForeignKeyField is defined.
129
162
  def apply_fk_registry!
130
- class_simple_name = self.name.split("::").last
163
+ # Anonymous classes (Class.new(Tina4::ORM), common in specs) have a nil
164
+ # name and can never be a string-form ForeignKeyField target, so there
165
+ # is nothing to wire — bail out instead of raising on nil.split.
166
+ return if name.nil?
167
+
168
+ class_simple_name = name.split("::").last
131
169
  return unless defined?(@@_fk_registry) && @@_fk_registry.key?(class_simple_name)
132
170
 
133
171
  @@_fk_registry[class_simple_name].each do |entry|
data/lib/tina4/orm.rb CHANGED
@@ -16,6 +16,29 @@ module Tina4
16
16
  class ORM
17
17
  include Tina4::FieldTypes
18
18
 
19
+ # When a new model class is defined, resolve any deferred ForeignKeyField
20
+ # wiring that targets it. The string / forward-reference form of
21
+ # `foreign_key_field` (e.g. `references: "Author"`) records the has_many
22
+ # side in @@_fk_registry but cannot wire it until the referenced class
23
+ # actually loads — which is now. Without this hook apply_fk_registry! was
24
+ # never called, so the has_many side silently never wired. The class body
25
+ # (where the model's own foreign_key_field declarations run, populating the
26
+ # registry) executes AFTER inherited returns, so entries keyed on THIS
27
+ # class were already recorded by earlier-loaded models. Chain through super
28
+ # so we never clobber a future inherited hook.
29
+ def self.inherited(subclass)
30
+ super
31
+ (@_model_subclasses ||= []) << subclass
32
+ subclass.apply_fk_registry! if subclass.respond_to?(:apply_fk_registry!, true)
33
+ end
34
+
35
+ # Every Tina4::ORM subclass that has been loaded, in definition order.
36
+ # Mirrors Python's ORM.__subclasses__() — used to resolve string-form
37
+ # ForeignKeyField references to a live class.
38
+ def self.model_subclasses
39
+ @_model_subclasses ||= []
40
+ end
41
+
19
42
  class << self
20
43
  def db
21
44
  # v3.13.12: implicit binding from TINA4_DATABASE_URL.
@@ -132,7 +155,7 @@ module Tina4
132
155
  #
133
156
  # @return [Tina4::QueryBuilder]
134
157
  def query
135
- QueryBuilder.from(table_name, db: db)
158
+ QueryBuilder.from_table(table_name, db: db)
136
159
  end
137
160
 
138
161
  def find(id_or_filter = nil, filter = nil, **kwargs)
data/lib/tina4/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.16"
4
+ VERSION = "3.13.18"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.13.16
4
+ version: 3.13.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team