tina4ruby 3.13.14 → 3.13.17

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: 0c7aa0bfb2393540123bc96f0251850236854b27b8cb64afbe3a57dc7036c952
4
- data.tar.gz: 675898e92f1d9fa832703afca63d833a2d895a37ce2d981963f24fe4a12ef451
3
+ metadata.gz: 198d9905bf51e280f45f2bba90aac40fd64ea1bdf0c0f0b29e9a80d5e95020c9
4
+ data.tar.gz: 74c01ef1ac870c47aebf57e686a9b3bd7efa46f5c56c1d78df4c378102ee39f2
5
5
  SHA512:
6
- metadata.gz: 51d150d7e359a5ec89e3daaf02b4d77b41c632fe8e57ad838fcebe3a517607aea026e4d1faa575c6b1b277631c1fa21e4c43c927c49de2b2859f79b1e9b04c46
7
- data.tar.gz: 5b002d55c8f5b822677b443ccffee40a88b5b46fe76bdd34df2e2ec3d2591f52010b93afd5ef6504cc22e55ce5d883726f80dba992a3886884a5f3a8e6acad42
6
+ metadata.gz: 03a840227106a3e2c12a78bde6c5d62ff8e4eb3041bc443fe73cad5e5d6d1dbd366ffa5458d34faeffe8448ad8fb19ce9bd74c821a4d3ec436ab80aa984b58fc
7
+ data.tar.gz: deefa98487ff855b8ad66beaa13b2e70f2b9f71677c0e317b52fc9153f43782a40723dc5519e814a2d65a29685fae18647f0bc84c124763aa8e5e7dcbddd07ca
@@ -405,6 +405,18 @@ module Tina4
405
405
  nil
406
406
  end
407
407
 
408
+ # Return the normalised engine name for this connection.
409
+ #
410
+ # Cross-framework parity with Python/PHP/Node ``get_database_type()``.
411
+ # ORM.create_table needs this to emit engine-correct DDL (SERIAL vs
412
+ # AUTOINCREMENT, BOOLEAN vs INTEGER, TIMESTAMP vs DATETIME). Returns the
413
+ # resolved driver key ("postgres", "mysql", "mssql", "firebird",
414
+ # "sqlite", ...) — the same alias-normalised value used to pick the
415
+ # driver class, so callers don't have to re-parse the connection string.
416
+ def get_database_type
417
+ @driver_name
418
+ end
419
+
408
420
  # Execute a write statement. Returns true/false for simple writes.
409
421
  # Returns DatabaseResult if SQL contains RETURNING, CALL, EXEC, or SELECT.
410
422
  def execute(sql, params = [])
@@ -39,8 +39,21 @@ module Tina4
39
39
  @records.empty?
40
40
  end
41
41
 
42
- def [](index)
43
- @records[index]
42
+ # Index / slice access into the result rows.
43
+ #
44
+ # ``result[0]`` is documented (book ch5 §4 "Index Access"). Delegating
45
+ # straight to the materialised rows means every Array subscript form
46
+ # works — ``result[0]``, ``result[-1]``, ``result[1, 2]`` and
47
+ # ``result[1..3]`` — and matches Python's ``DatabaseResult.__getitem__``,
48
+ # which forwards to its records list.
49
+ def [](*args)
50
+ @records[*args]
51
+ end
52
+
53
+ # Implicit array coercion, so a DatabaseResult can be splatted and used
54
+ # anywhere an Array is expected (``a, b = result``, ``[*result]``).
55
+ def to_ary
56
+ @records.dup
44
57
  end
45
58
 
46
59
  def length
@@ -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/orm.rb CHANGED
@@ -306,13 +306,29 @@ module Tina4
306
306
  def create_table
307
307
  return true if db.table_exists?(table_name)
308
308
 
309
- # v3.13.11 (BooleanField parity): pick each engine's native
310
- # bool type where it's reliable. SQLite has no native bool;
311
- # Firebird's driver round-trip for native BOOLEAN is uneven
312
- # both stay on INTEGER. PG/MySQL/MSSQL use their native types
313
- # so Python ``True``/Ruby ``true`` bind cleanly without
314
- # ``operator does not exist: boolean = integer`` errors.
309
+ # v3.13.16: engine-aware DDL. Ruby used to emit SQLite-only DDL on
310
+ # every driver INTEGER for booleans, DATETIME for datetimes, and a
311
+ # raw AUTOINCREMENT keyword then ignore db.execute()'s return value
312
+ # and report success. On PostgreSQL the CREATE blew up
313
+ # ("syntax error at or near AUTOINCREMENT"), db.execute() swallowed it
314
+ # into get_error() and returned false, yet create_table still returned
315
+ # true with no table created — a silent, misleading pass.
316
+ #
317
+ # The fix mirrors the Python reference (tina4_python.orm.model):
318
+ # • get_database_type() now exists on Database (it didn't before, so
319
+ # the v3.13.11 BooleanField check never actually fired on Ruby).
320
+ # • BooleanField → native BOOLEAN (PG/MySQL) / BIT (MSSQL) /
321
+ # INTEGER (sqlite, firebird) — both PG aliases are matched.
322
+ # • DateTimeField → TIMESTAMP on PG/Firebird (neither has a DATETIME
323
+ # type), DATETIME elsewhere.
324
+ # • boolean DEFAULT is engine-aware: TRUE/FALSE for a native BOOLEAN,
325
+ # 1/0 for INTEGER/BIT-backed bools.
326
+ # • AUTOINCREMENT is translated per engine via SQLTranslator
327
+ # (SERIAL on PG, AUTO_INCREMENT on MySQL, IDENTITY on MSSQL, dropped
328
+ # on Firebird) instead of being emitted raw.
329
+ # • return false (not true) when the DDL fails.
315
330
  engine = (db.respond_to?(:get_database_type) ? db.get_database_type : "").to_s.downcase
331
+
316
332
  bool_sql = case engine
317
333
  when "postgres", "postgresql" then "BOOLEAN"
318
334
  when "mysql" then "BOOLEAN" # alias for TINYINT(1)
@@ -320,6 +336,15 @@ module Tina4
320
336
  else "INTEGER" # sqlite, firebird, odbc, anything else
321
337
  end
322
338
 
339
+ # PostgreSQL and Firebird have no DATETIME type — CREATE TABLE fails
340
+ # with `type "datetime" does not exist`. Emit each engine's real
341
+ # timestamp type. (MySQL/MSSQL/SQLite keep DATETIME: valid there, and
342
+ # on MySQL it avoids TIMESTAMP's auto-update + 2038 surprises.)
343
+ datetime_sql = case engine
344
+ when "postgres", "postgresql", "firebird" then "TIMESTAMP"
345
+ else "DATETIME"
346
+ end
347
+
323
348
  type_map = {
324
349
  integer: "INTEGER",
325
350
  string: "VARCHAR(255)",
@@ -328,7 +353,7 @@ module Tina4
328
353
  decimal: "REAL",
329
354
  boolean: bool_sql,
330
355
  date: "DATE",
331
- datetime: "DATETIME",
356
+ datetime: datetime_sql,
332
357
  timestamp: "TIMESTAMP",
333
358
  blob: "BLOB",
334
359
  json: "TEXT"
@@ -346,15 +371,29 @@ module Tina4
346
371
  parts << "AUTOINCREMENT" if opts[:auto_increment]
347
372
  parts << "NOT NULL" if !opts[:nullable] && !opts[:primary_key]
348
373
  if opts[:default] && !opts[:auto_increment]
349
- default_val = opts[:default].is_a?(String) ? "'#{opts[:default]}'" : opts[:default]
350
- parts << "DEFAULT #{default_val}"
374
+ parts << "DEFAULT #{default_literal(opts[:default], opts[:type], bool_sql)}"
351
375
  end
352
376
  col_defs << parts.join(" ")
353
377
  end
354
378
 
355
379
  sql = "CREATE TABLE IF NOT EXISTS #{table_name} (#{col_defs.join(', ')})"
356
- db.execute(sql)
380
+
381
+ # Translate AUTOINCREMENT to the engine's auto-increment syntax
382
+ # (INTEGER PRIMARY KEY AUTOINCREMENT -> SERIAL PRIMARY KEY on PG, etc.).
383
+ # SQLTranslator keys off the -ql spelling for postgres.
384
+ translator_engine = %w[postgres postgresql].include?(engine) ? "postgresql" : engine
385
+ sql = SQLTranslator.auto_increment_syntax(sql, translator_engine)
386
+
387
+ # Don't claim success when the DDL failed. db.execute() swallows the
388
+ # driver error into get_error() and returns false, so a bad type (or
389
+ # any DDL error) used to leave create_table returning true while no
390
+ # table was actually created.
391
+ ok = db.execute(sql)
357
392
  db.commit
393
+ if ok == false
394
+ Tina4::Log.error("create_table failed for #{table_name}: #{db.get_error}", { sql: sql })
395
+ return false
396
+ end
358
397
  true
359
398
  end
360
399
 
@@ -427,6 +466,28 @@ module Tina4
427
466
  results = db.fetch(sql, filter.values)
428
467
  results.map { |row| from_hash(row) }
429
468
  end
469
+
470
+ # Render a column DEFAULT literal for create_table.
471
+ #
472
+ # Strings are quoted. Booleans are engine-aware: a native BOOLEAN
473
+ # column (PG/MySQL) needs TRUE/FALSE, while INTEGER- and BIT-backed
474
+ # bools (SQLite, Firebird, MSSQL) need 1/0 — `DEFAULT 0` on a PG
475
+ # BOOLEAN raises "default expression is of type integer". Everything
476
+ # else is emitted as-is.
477
+ def default_literal(value, type, bool_sql)
478
+ if value.is_a?(String)
479
+ "'#{value}'"
480
+ elsif value == true || value == false || type == :boolean
481
+ truthy = value == true || value == 1 || value == "1"
482
+ if bool_sql == "BOOLEAN"
483
+ truthy ? "TRUE" : "FALSE"
484
+ else
485
+ truthy ? "1" : "0"
486
+ end
487
+ else
488
+ value.to_s
489
+ end
490
+ end
430
491
  end
431
492
 
432
493
  def initialize(attributes = {})
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.14"
4
+ VERSION = "3.13.17"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.13.14
4
+ version: 3.13.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-13 00:00:00.000000000 Z
11
+ date: 2026-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack