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 +4 -4
- data/lib/tina4/drivers/postgres_driver.rb +51 -0
- data/lib/tina4/field_types.rb +40 -2
- data/lib/tina4/orm.rb +24 -1
- data/lib/tina4/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0073552dcac0e437fa36de2038f6958cbdbcde9077ec2427d9fd1bf77242da0e
|
|
4
|
+
data.tar.gz: c5c667382a67031bef1d7f3646020984942079edb5de9c5084c135ce4be68e9d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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}" }
|
data/lib/tina4/field_types.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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