tina4ruby 3.13.124 → 3.13.126
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/cli.rb +2 -2
- data/lib/tina4/dev_admin.rb +3 -1
- data/lib/tina4/drivers/firebird_driver.rb +11 -0
- data/lib/tina4/drivers/mssql_driver.rb +8 -0
- data/lib/tina4/drivers/mysql_driver.rb +9 -1
- data/lib/tina4/seeder.rb +65 -6
- data/lib/tina4/sql_translator.rb +46 -0
- 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: f56bea80e9f63a3326a77f8903721890f45a2db216f39c24fe51c1363e0514b9
|
|
4
|
+
data.tar.gz: 61dd38932bcfb406abb603232dd584889223408888d3ca7aaa469775243727b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6ca97a0dcb6adf6776f89be49871d814b678a3d499d8ec740a6b87516af474f04d1e78d2c8746a113aa8877c2b3fcceec4ebd2f9d3cc02b2f4fd0accc63d095
|
|
7
|
+
data.tar.gz: 5c86231d8853e00c5cbbdf22a6ea02d477eed2fe045306c498ed1b82eff9ba5bc9c1e301b53f99a394f82731001c56503615206d566e2266b6e4208c2d64aecb
|
data/lib/tina4/cli.rb
CHANGED
|
@@ -143,7 +143,7 @@ module Tina4
|
|
|
143
143
|
"bool" => { orm: "boolean_field", sql: "INTEGER", default: "0" },
|
|
144
144
|
"boolean" => { orm: "boolean_field", sql: "INTEGER", default: "0" },
|
|
145
145
|
"text" => { orm: "string_field", sql: "TEXT", default: "''" },
|
|
146
|
-
"datetime" => { orm: "string_field", sql: "
|
|
146
|
+
"datetime" => { orm: "string_field", sql: "TIMESTAMP", default: "NULL" },
|
|
147
147
|
"blob" => { orm: "string_field", sql: "BLOB", default: "NULL" },
|
|
148
148
|
}.freeze
|
|
149
149
|
|
|
@@ -2051,7 +2051,7 @@ module Tina4
|
|
|
2051
2051
|
default = info[:default] != "NULL" ? " DEFAULT #{info[:default]}" : ""
|
|
2052
2052
|
col_lines << " #{fname} #{info[:sql]}#{default}"
|
|
2053
2053
|
end
|
|
2054
|
-
col_lines << " created_at
|
|
2054
|
+
col_lines << " created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
|
2055
2055
|
|
|
2056
2056
|
# ADR-0063: `-- tina4:edit` marker for the SQL scanner (mirrors `# tina4:edit`
|
|
2057
2057
|
# in Ruby files). The scanner regex accepts both comment prefixes.
|
data/lib/tina4/dev_admin.rb
CHANGED
|
@@ -1385,7 +1385,9 @@ module Tina4
|
|
|
1385
1385
|
# the closure, not a dead keyword.
|
|
1386
1386
|
fake = Tina4::FakeData.new(seed: seed)
|
|
1387
1387
|
field_map = Tina4._normalize_columns(db.columns(table_name)).each_with_object({}) do |(col_name, type_sym), map|
|
|
1388
|
-
|
|
1388
|
+
# Thread the TABLE name so a generic name column on a product-ish
|
|
1389
|
+
# table seeds a product name (parity with the seed_table path).
|
|
1390
|
+
map[col_name] = -> { fake.for_field({ type: type_sym }, col_name, table_name) }
|
|
1389
1391
|
end
|
|
1390
1392
|
# Delegate to the shared resilient seed_table helper so the endpoint
|
|
1391
1393
|
# gets the exact same per-row wrap (P1) — no unhandled row failure can
|
|
@@ -231,6 +231,17 @@ module Tina4
|
|
|
231
231
|
end
|
|
232
232
|
|
|
233
233
|
def execute(sql, params = [])
|
|
234
|
+
# Translate SQLite-canonical DDL to Firebird at APPLY time, mirroring the
|
|
235
|
+
# Python master's firebird.py _translate_sql: strip AUTOINCREMENT (Firebird
|
|
236
|
+
# uses generators), then rewrite the types Firebird rejects -- TEXT ->
|
|
237
|
+
# BLOB SUB_TYPE TEXT (-607), REAL -> DOUBLE PRECISION -- and drop
|
|
238
|
+
# CREATE TABLE IF NOT EXISTS. Both helpers only touch DDL keywords
|
|
239
|
+
# (AUTOINCREMENT / CREATE TABLE / ALTER TABLE), so a plain
|
|
240
|
+
# INSERT/UPDATE/DELETE/SELECT is returned untouched. This is what lets a
|
|
241
|
+
# REALLY-generated migration (id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
242
|
+
# bio TEXT, price REAL) apply on Firebird instead of raising -607/-104.
|
|
243
|
+
sql = Tina4::SQLTranslator.auto_increment_syntax(sql, "firebird")
|
|
244
|
+
sql = Tina4::SQLTranslator.ddl_types(sql, "firebird")
|
|
234
245
|
result = with_reconnect do
|
|
235
246
|
if params.empty?
|
|
236
247
|
@connection.execute(sql)
|
|
@@ -59,6 +59,14 @@ module Tina4
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def execute(sql, params = [])
|
|
62
|
+
# Translate SQLite-canonical DDL to T-SQL at APPLY time, mirroring the
|
|
63
|
+
# Python master's mssql.py _translate_sql: AUTOINCREMENT -> IDENTITY(1,1),
|
|
64
|
+
# then strip CREATE TABLE IF NOT EXISTS (not T-SQL) and map TIMESTAMP ->
|
|
65
|
+
# DATETIME2 (MSSQL's TIMESTAMP is a rowversion, not a datetime). Both
|
|
66
|
+
# helpers only touch DDL keywords, so DML is returned untouched -- this is
|
|
67
|
+
# what lets a REALLY-generated migration apply on MSSQL.
|
|
68
|
+
sql = Tina4::SQLTranslator.auto_increment_syntax(sql, "mssql")
|
|
69
|
+
sql = Tina4::SQLTranslator.ddl_types(sql, "mssql")
|
|
62
70
|
effective_sql = interpolate_params(sql, params)
|
|
63
71
|
|
|
64
72
|
# Capture the generated IDENTITY AT WRITE TIME — mirror of the Python
|
|
@@ -73,7 +73,15 @@ module Tina4
|
|
|
73
73
|
# wiring that makes the previously-dead concat/ilike helpers live in Ruby.)
|
|
74
74
|
def translate_dialect(sql)
|
|
75
75
|
sql = Tina4::SQLTranslator.concat_pipes_to_func(sql)
|
|
76
|
-
Tina4::SQLTranslator.ilike_to_like(sql)
|
|
76
|
+
sql = Tina4::SQLTranslator.ilike_to_like(sql)
|
|
77
|
+
# Complete the DDL dialect translation at APPLY time, mirroring the Python
|
|
78
|
+
# master's mysql.py _translate_sql: AUTOINCREMENT -> AUTO_INCREMENT, then
|
|
79
|
+
# map a datetime column's TIMESTAMP -> DATETIME (MySQL's TIMESTAMP carries
|
|
80
|
+
# auto-update + 2038 surprises). Both helpers only touch DDL keywords, so a
|
|
81
|
+
# SELECT/INSERT is returned untouched -- this is what lets a
|
|
82
|
+
# REALLY-generated migration apply on MySQL.
|
|
83
|
+
sql = Tina4::SQLTranslator.auto_increment_syntax(sql, "mysql")
|
|
84
|
+
Tina4::SQLTranslator.ddl_types(sql, "mysql")
|
|
77
85
|
end
|
|
78
86
|
|
|
79
87
|
def execute_query(sql, params = [])
|
data/lib/tina4/seeder.rb
CHANGED
|
@@ -108,6 +108,30 @@ module Tina4
|
|
|
108
108
|
CURRENCIES = %w[USD EUR GBP JPY CAD AUD CHF ZAR INR CNY].freeze
|
|
109
109
|
CREDIT_CARD_PREFIXES = %w[4111 4242 5500 5105].freeze
|
|
110
110
|
|
|
111
|
+
# Product-name vocabulary (adjective + noun). Seeds a generic
|
|
112
|
+
# +name+/+full_name+ column on a product-ish table with "Wireless Keyboard"
|
|
113
|
+
# instead of a person name. Mirrors the Python master's word banks.
|
|
114
|
+
PRODUCT_ADJECTIVES = %w[
|
|
115
|
+
Wireless Organic Premium Classic Eco Smart Portable
|
|
116
|
+
Deluxe Compact Rustic Handcrafted Vintage Modern
|
|
117
|
+
Ergonomic Stainless Bamboo Recycled Artisan Professional
|
|
118
|
+
Ultra Insulated Lightweight Adjustable Foldable
|
|
119
|
+
].freeze
|
|
120
|
+
|
|
121
|
+
PRODUCT_NOUNS = [
|
|
122
|
+
"Keyboard", "Coffee Beans", "Backpack", "Water Bottle", "Desk Lamp",
|
|
123
|
+
"Headphones", "Notebook", "Sneakers", "Sunglasses", "Wallet", "Mug",
|
|
124
|
+
"Chair", "Blender", "Speaker", "Charger", "Umbrella", "Toothbrush",
|
|
125
|
+
"Jacket", "Watch", "Kettle", "Picture Frame", "Planter", "Cutlery Set",
|
|
126
|
+
"Yoga Mat", "Phone Case"
|
|
127
|
+
].freeze
|
|
128
|
+
|
|
129
|
+
# A table/model whose name contains any of these gets product names on its
|
|
130
|
+
# generic +name+/+full_name+ column instead of a person name.
|
|
131
|
+
PRODUCT_TABLE_HINTS = %w[
|
|
132
|
+
product item catalog inventory goods merchandise sku listing stock ware
|
|
133
|
+
].freeze
|
|
134
|
+
|
|
111
135
|
def initialize(seed: nil)
|
|
112
136
|
@rng = seed ? Random.new(seed) : Random.new
|
|
113
137
|
end
|
|
@@ -247,6 +271,23 @@ module Tina4
|
|
|
247
271
|
JOB_TITLES[@rng.rand(JOB_TITLES.length)]
|
|
248
272
|
end
|
|
249
273
|
|
|
274
|
+
# A plausible product name, e.g. "Wireless Keyboard" or "Organic Coffee
|
|
275
|
+
# Beans". Deterministic under a seed like every other generator (draws from
|
|
276
|
+
# the same @rng), so a seeded run reproduces it. Mirrors the Python master's
|
|
277
|
+
# FakeData.product().
|
|
278
|
+
def product
|
|
279
|
+
"#{PRODUCT_ADJECTIVES[@rng.rand(PRODUCT_ADJECTIVES.length)]} #{PRODUCT_NOUNS[@rng.rand(PRODUCT_NOUNS.length)]}"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# True when the table/model name looks like a product catalogue, so a generic
|
|
283
|
+
# +name+/+full_name+ column seeds a product name, not a person name. With no
|
|
284
|
+
# table context (nil/"") this is false, so the person-name default is kept
|
|
285
|
+
# (back-compat). Mirrors the Python master's +_is_product_table+.
|
|
286
|
+
def self.product_table?(table)
|
|
287
|
+
down = table.to_s.downcase
|
|
288
|
+
PRODUCT_TABLE_HINTS.any? { |hint| down.include?(hint) }
|
|
289
|
+
end
|
|
290
|
+
|
|
250
291
|
def currency
|
|
251
292
|
CURRENCIES[@rng.rand(CURRENCIES.length)]
|
|
252
293
|
end
|
|
@@ -282,7 +323,13 @@ module Tina4
|
|
|
282
323
|
end
|
|
283
324
|
|
|
284
325
|
# Generate appropriate data based on field definition and column name.
|
|
285
|
-
|
|
326
|
+
#
|
|
327
|
+
# +table+ (optional) is the table/model name. When it names a product-ish
|
|
328
|
+
# catalogue (see FakeData.product_table?) a generic +name+/+full_name+/
|
|
329
|
+
# +fullname+ column yields a product name ("Wireless Keyboard") instead of a
|
|
330
|
+
# person name. With no table context the person-name default is kept
|
|
331
|
+
# (back-compat). first_name/last_name/user_name stay person either way.
|
|
332
|
+
def for_field(field_def, column_name = nil, table = nil)
|
|
286
333
|
col = (column_name || "").to_s.downcase
|
|
287
334
|
type = field_def[:type]
|
|
288
335
|
|
|
@@ -323,7 +370,7 @@ module Tina4
|
|
|
323
370
|
|
|
324
371
|
when :string, :text
|
|
325
372
|
max_len = field_def[:length] || 255
|
|
326
|
-
val = generate_string_for(col, max_len)
|
|
373
|
+
val = generate_string_for(col, max_len, table)
|
|
327
374
|
val.length > max_len ? val[0...max_len] : val
|
|
328
375
|
|
|
329
376
|
else
|
|
@@ -333,9 +380,15 @@ module Tina4
|
|
|
333
380
|
|
|
334
381
|
private
|
|
335
382
|
|
|
336
|
-
def generate_string_for(col, max_len)
|
|
383
|
+
def generate_string_for(col, max_len, table = nil)
|
|
337
384
|
return email[0...max_len] if col.include?("email")
|
|
338
|
-
|
|
385
|
+
if %w[name full_name fullname display_name].include?(col)
|
|
386
|
+
# GENERIC full-name column: a product-ish table gets a product name, any
|
|
387
|
+
# other table (or no table context) keeps the person name. first_name/
|
|
388
|
+
# last_name/user_name are handled by the branches below, so they stay
|
|
389
|
+
# person on a product table too.
|
|
390
|
+
return (self.class.product_table?(table) ? product : name)[0...max_len]
|
|
391
|
+
end
|
|
339
392
|
return first_name[0...max_len] if col.include?("first") && col.include?("name")
|
|
340
393
|
return last_name[0...max_len] if col.include?("last") && col.include?("name")
|
|
341
394
|
return last_name[0...max_len] if col =~ /surname|family_name/
|
|
@@ -523,7 +576,10 @@ module Tina4
|
|
|
523
576
|
elsif fk_pools[name] && !fk_pools[name].empty?
|
|
524
577
|
attrs[name] = fake.choice(fk_pools[name])
|
|
525
578
|
else
|
|
526
|
-
|
|
579
|
+
# Thread the MODEL name (orm_class.name) so a generic name column on
|
|
580
|
+
# a product-ish model seeds a product name (parity with the Python
|
|
581
|
+
# master, which passes orm_class.__name__).
|
|
582
|
+
generated = fake.for_field(field_def, name, orm_class.name)
|
|
527
583
|
attrs[name] = generated unless generated.nil?
|
|
528
584
|
end
|
|
529
585
|
end
|
|
@@ -614,7 +670,10 @@ module Tina4
|
|
|
614
670
|
row[col_name] = type_str.arity.zero? ? type_str.call : type_str.call(fake)
|
|
615
671
|
else
|
|
616
672
|
field_def = { type: type_str.to_sym }
|
|
617
|
-
|
|
673
|
+
# Thread the TABLE name so a generic name column on a product-ish
|
|
674
|
+
# table (incl. the MCP seed_table dev tool, which reaches here via
|
|
675
|
+
# db.columns) seeds a product name instead of a person name.
|
|
676
|
+
row[col_name] = fake.for_field(field_def, col_name, table_name)
|
|
618
677
|
end
|
|
619
678
|
end
|
|
620
679
|
|
data/lib/tina4/sql_translator.rb
CHANGED
|
@@ -233,6 +233,52 @@ module Tina4
|
|
|
233
233
|
end
|
|
234
234
|
end
|
|
235
235
|
|
|
236
|
+
# Translate SQLite-canonical DDL column TYPES + CREATE-TABLE options to the
|
|
237
|
+
# target engine.
|
|
238
|
+
#
|
|
239
|
+
# ONLY acts on +CREATE TABLE+ / +ALTER TABLE+ statements, so a query or
|
|
240
|
+
# INSERT that happens to contain the word +TEXT+ (a column name, a string
|
|
241
|
+
# literal) is never rewritten. Complements +auto_increment_syntax+ (which
|
|
242
|
+
# maps the id keyword) so ONE portable migration -- and every
|
|
243
|
+
# +ORM.create_table+ DDL, which is also SQLite-canonical -- applies on every
|
|
244
|
+
# engine instead of failing on Firebird/MSSQL.
|
|
245
|
+
#
|
|
246
|
+
# * Firebird has no +TEXT+ (-607), no +REAL+, and no
|
|
247
|
+
# +CREATE TABLE IF NOT EXISTS+.
|
|
248
|
+
# * MSSQL has no +CREATE TABLE IF NOT EXISTS+ and its +TIMESTAMP+ is a
|
|
249
|
+
# rowversion, not a datetime -- a +created_at TIMESTAMP+ there is wrong.
|
|
250
|
+
# * MySQL's +TIMESTAMP+ carries auto-update / 2038 surprises, so a datetime
|
|
251
|
+
# column maps to +DATETIME+ (matching ORM.create_table).
|
|
252
|
+
#
|
|
253
|
+
# @param sql [String]
|
|
254
|
+
# @param engine [String] firebird, mssql, mysql (others pass through)
|
|
255
|
+
# @return [String]
|
|
256
|
+
def ddl_types(sql, engine)
|
|
257
|
+
# Gate to DDL only, tolerating leading `-- ...` comment lines / blank
|
|
258
|
+
# lines that a migration file carries before its CREATE TABLE. A SELECT
|
|
259
|
+
# or INSERT that merely mentions a type keyword is never rewritten.
|
|
260
|
+
head = sql.sub(/\A(?:\s*--[^\n]*\n)+/, "")
|
|
261
|
+
return sql unless head =~ /\A\s*(?:CREATE\s+TABLE|ALTER\s+TABLE)\b/i
|
|
262
|
+
|
|
263
|
+
case engine.to_s.downcase
|
|
264
|
+
when "firebird"
|
|
265
|
+
sql = sql.gsub(/\bIF\s+NOT\s+EXISTS\b/i, "")
|
|
266
|
+
# Map bare TEXT -> BLOB SUB_TYPE TEXT, but leave an existing
|
|
267
|
+
# "BLOB SUB_TYPE TEXT" intact (it already contains the word TEXT).
|
|
268
|
+
sql = sql.gsub(/\bBLOB\s+SUB_TYPE\s+TEXT\b/i, "\x00FBTEXT\x00")
|
|
269
|
+
sql = sql.gsub(/\bTEXT\b/i, "BLOB SUB_TYPE TEXT")
|
|
270
|
+
sql = sql.gsub("\x00FBTEXT\x00", "BLOB SUB_TYPE TEXT")
|
|
271
|
+
sql.gsub(/\bREAL\b/i, "DOUBLE PRECISION")
|
|
272
|
+
when "mssql"
|
|
273
|
+
sql = sql.gsub(/\bIF\s+NOT\s+EXISTS\b/i, "")
|
|
274
|
+
sql.gsub(/\bTIMESTAMP\b/i, "DATETIME2")
|
|
275
|
+
when "mysql"
|
|
276
|
+
sql.gsub(/\bTIMESTAMP\b/i, "DATETIME")
|
|
277
|
+
else
|
|
278
|
+
sql
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
236
282
|
# NOTE (SQLTRANS-DEC-02): +placeholder_style+ was removed - every Ruby
|
|
237
283
|
# driver owns its own placeholder shape (+?+ for MySQL/SQLite/MSSQL/Firebird,
|
|
238
284
|
# +$1+ for Postgres), so this helper had zero callers and disagreed with the
|
data/lib/tina4/version.rb
CHANGED