diff --git a/Makefile b/Makefile index 366b23b752..b45c3c0a82 100644 --- a/Makefile +++ b/Makefile @@ -42,3 +42,4 @@ deps: $(MAKE) deps -C ./python/sqlalchemy $(MAKE) deps -C ./ruby/activerecord $(MAKE) deps -C ./ruby/ar4 + $(MAKE) deps -C ./python/django diff --git a/python/django/Makefile b/python/django/Makefile new file mode 100644 index 0000000000..b602025965 --- /dev/null +++ b/python/django/Makefile @@ -0,0 +1,8 @@ +.PHONY: start +start: + ./manage.py migrate cockroach_example && ./manage.py runserver 6543 + +deps: + git clone https://github.com/cockroachlabs/cockroach-django || true + cd cockroach-django && pip install . + python -m pip install "django<2" diff --git a/python/django/cockroach_example/__init__.py b/python/django/cockroach_example/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/django/cockroach_example/migrations/0001_initial.py b/python/django/cockroach_example/migrations/0001_initial.py new file mode 100644 index 0000000000..a1509749fb --- /dev/null +++ b/python/django/cockroach_example/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 2.2.6 on 2019-10-02 18:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Customers', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=250)), + ], + ), + migrations.CreateModel( + name='Products', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=250)), + ('price', models.DecimalField(decimal_places=2, max_digits=18)), + ], + ), + migrations.CreateModel( + name='Orders', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('subtotal', models.DecimalField(decimal_places=2, max_digits=18)), + ('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='cockroach_example.Customers')), + ('product', models.ManyToManyField(to='cockroach_example.Products')), + ], + ), + ] diff --git a/python/django/cockroach_example/migrations/__init__.py b/python/django/cockroach_example/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/django/cockroach_example/models.py b/python/django/cockroach_example/models.py new file mode 100644 index 0000000000..0ddeb593e8 --- /dev/null +++ b/python/django/cockroach_example/models.py @@ -0,0 +1,18 @@ +from django.db import models + +class Customers(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=250) + +class Products(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=250) + price = models.DecimalField(max_digits=18, decimal_places=2) + +class Orders(models.Model): + id = models.AutoField(primary_key=True) + subtotal = models.DecimalField(max_digits=18, decimal_places=2) + # TODO (rohany): is setting null the right thing here? The schema doesn't say what to do. + customer = models.ForeignKey(Customers, on_delete=models.SET_NULL, null=True) + product = models.ManyToManyField(Products) + diff --git a/python/django/cockroach_example/settings.py b/python/django/cockroach_example/settings.py new file mode 100644 index 0000000000..ee84844b71 --- /dev/null +++ b/python/django/cockroach_example/settings.py @@ -0,0 +1,135 @@ +""" +Django settings for cockroach_example project. + +Generated by 'django-admin startproject' using Django 2.2.6. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.2/ref/settings/ +""" + +import os + + +from urlparse import urlparse + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '0pld^66i)iv4df8km5vc%1^sskuqjf16jk&z=c^rk--oh6i0i^' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'cockroach_example', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'cockroach_example.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'cockroach_example.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/2.2/ref/settings/#databases + +port = 26257 +addr = os.getenv('ADDR') +if addr is not None: + url = urlparse(addr) + port = url.port + +DATABASES = { + 'default': { + # 'ENGINE' : 'django.db.backends.postgresql', + 'ENGINE' : 'cockroach.django', + 'NAME' : 'company_django', + 'USER' : 'root', + 'PASSWORD': '', + 'HOST' : 'localhost', + 'PORT' : port, + } +} + + +# Password validation +# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/2.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.2/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/python/django/cockroach_example/urls.py b/python/django/cockroach_example/urls.py new file mode 100644 index 0000000000..b6c8668a1b --- /dev/null +++ b/python/django/cockroach_example/urls.py @@ -0,0 +1,35 @@ +"""cockroach_example URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.conf.urls import url + +from .views import CustomersView, OrdersView, PingView, ProductView + +urlpatterns = [ + url('admin/', admin.site.urls), + + url('ping/', PingView.as_view()), + + # Endpoints for customers URL. + url('customer/', CustomersView.as_view(), name='customers'), + url('customer//', CustomersView.as_view(), name='customers'), + + # Endpoints for customers URL. + url('product/', ProductView.as_view(), name='product'), + url('product//', ProductView.as_view(), name='product'), + + url('order/', OrdersView.as_view(), name='order'), +] diff --git a/python/django/cockroach_example/views.py b/python/django/cockroach_example/views.py new file mode 100644 index 0000000000..9577001094 --- /dev/null +++ b/python/django/cockroach_example/views.py @@ -0,0 +1,77 @@ +from django.http import JsonResponse, HttpResponse +from django.utils.decorators import method_decorator +from django.views.generic import View +from django.views.decorators.csrf import csrf_exempt + +import json +import sys + +from .models import * + +class PingView(View): + def get(self, request, *args, **kwargs): + return HttpResponse("python/django", status=200) + +@method_decorator(csrf_exempt, name='dispatch') +class CustomersView(View): + def get(self, request, id=None, *args, **kwargs): + if id is None: + customers = list(Customers.objects.values()) + else: + customers = list(Customers.objects.filter(id=id).values()) + return JsonResponse(customers, safe=False) + + def post(self, request, *args, **kwargs): + form_data = json.loads(request.body.decode()) + name = form_data['name'] + c = Customers(name=name) + c.save() + return HttpResponse(status=200) + + def delete(self, request, id=None, *args, **kwargs): + if id is None: + return HttpResponse(status=404) + Customers.objects.filter(id=id).delete() + return HttpResponse(status=200) + + # The PUT method is shadowed by the POST method, so there doesn't seem + # to be a reason to include it. + +@method_decorator(csrf_exempt, name='dispatch') +class ProductView(View): + def get(self, request, id=None, *args, **kwargs): + if id is None: + products = list(Products.objects.values()) + else: + products = list(Products.objects.filter(id=id).values()) + return JsonResponse(products, safe=False) + + def post(self, request, *args, **kwargs): + form_data = json.loads(request.body.decode()) + name, price = form_data['name'], form_data['price'] + p = Products(name=name, price=price) + p.save() + return HttpResponse(status=200) + + # The REST API outlined in the github does not say that /product/ needs + # a PUT and DELETE method + +@method_decorator(csrf_exempt, name='dispatch') +class OrdersView(View): + def get(self, request, id=None, *args, **kwargs): + if id is None: + orders = list(Orders.objects.values()) + else: + orders = list(Orders.objects.filter(id=id).values()) + return JsonResponse(orders, safe=False) + + def post(self, request, *args, **kwargs): + form_data = json.loads(request.body.decode()) + c = Customers.objects.get(id=form_data['customer']['id']) + o = Orders(subtotal=form_data['subtotal'], customer=c) + o.save() + for p in form_data['products']: + p = Products.objects.get(id=p['id']) + o.product.add(p) + o.save() + return HttpResponse(status=200) diff --git a/python/django/cockroach_example/wsgi.py b/python/django/cockroach_example/wsgi.py new file mode 100644 index 0000000000..31853b7737 --- /dev/null +++ b/python/django/cockroach_example/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for cockroach_example project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cockroach_example.settings') + +application = get_wsgi_application() diff --git a/python/django/manage.py b/python/django/manage.py new file mode 100755 index 0000000000..34ca6cb140 --- /dev/null +++ b/python/django/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cockroach_example.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/python/sqlalchemy/server.py b/python/sqlalchemy/server.py index c315c03faa..075f17e146 100755 --- a/python/sqlalchemy/server.py +++ b/python/sqlalchemy/server.py @@ -65,7 +65,7 @@ def setup_app(): app = setup_app() -@app.route('/ping') +@app.route('/ping/') def ping(): return 'python/sqlalchemy' @@ -73,14 +73,14 @@ def ping(): # The following functions respond to various HTTP routes, as described in the # top-level README.md. -@app.route('/customer', methods=['GET']) +@app.route('/customer/', methods=['GET']) def get_customers(): customers = [c.as_dict() for c in Customer.query.all()] return Response(json.dumps(customers), 200, mimetype="application/json; charset=UTF-8") -@app.route('/customer', methods=['POST']) +@app.route('/customer/', methods=['POST']) def create_customer(): try: body = request.stream.read().decode('utf-8') @@ -93,7 +93,7 @@ def create_customer(): return body -@app.route('/customer/', methods=['GET']) +@app.route('/customer//', methods=['GET']) def get_customer(id=None): if id is None: return Response('no ID specified', 400) @@ -102,14 +102,14 @@ def get_customer(id=None): mimetype="application/json; charset=UTF-8") -@app.route('/order', methods=['GET']) +@app.route('/order/', methods=['GET']) def get_orders(): orders = [o.as_dict() for o in Order.query.all()] return Response(json.dumps(orders), 200, mimetype="application/json; charset=UTF-8") -@app.route('/order', methods=['POST']) +@app.route('/order/', methods=['POST']) def create_order(): try: body = request.stream.read().decode('utf-8') @@ -125,7 +125,7 @@ def create_order(): return body -@app.route('/order/', methods=['GET']) +@app.route('/order//', methods=['GET']) def get_order(id=None): if id is None: return Response('no ID specified', 400) @@ -134,14 +134,14 @@ def get_order(id=None): mimetype="application/json; charset=UTF-8") -@app.route('/product', methods=['GET']) +@app.route('/product/', methods=['GET']) def get_products(): products = [p.as_dict() for p in Product.query.all()] return Response(json.dumps(products), 200, mimetype="application/json; charset=UTF-8") -@app.route('/product', methods=['POST']) +@app.route('/product/', methods=['POST']) def create_product(): try: body = request.stream.read().decode('utf-8') @@ -154,7 +154,7 @@ def create_product(): return body -@app.route('/product/', methods=['GET']) +@app.route('/product//', methods=['GET']) def get_product(id=None): if id is None: return Response('no ID specified', 400) diff --git a/testing/api_handler.go b/testing/api_handler.go index 7f0edcac99..04c63375a7 100644 --- a/testing/api_handler.go +++ b/testing/api_handler.go @@ -8,19 +8,18 @@ import ( "net/http" "strings" - "github.com/pkg/errors" - "github.com/cockroachdb/examples-orms/go/gorm/model" + "github.com/pkg/errors" ) const ( applicationAddr = "localhost:6543" applicationURL = "http://" + applicationAddr - pingPath = applicationURL + "/ping" - customersPath = applicationURL + "/customer" - ordersPath = applicationURL + "/order" - productsPath = applicationURL + "/product" + pingPath = applicationURL + "/ping/" + customersPath = applicationURL + "/customer/" + ordersPath = applicationURL + "/order/" + productsPath = applicationURL + "/product/" jsonContentType = "application/json" ) diff --git a/testing/main_test.go b/testing/main_test.go index bcf1ec292f..5c4e72cb93 100644 --- a/testing/main_test.go +++ b/testing/main_test.go @@ -12,7 +12,6 @@ import ( "time" "github.com/cockroachdb/cockroach-go/testserver" - // Import postgres driver. _ "github.com/lib/pq" ) @@ -140,7 +139,9 @@ func initORMApp(app application, dbURL *url.URL) (func() error, error) { } } -func testORM(t *testing.T, language, orm string) { +func testORM( + t *testing.T, language, orm string, tableNames testTableNames, columnNames testColumnNames, +) { app := application{ language: language, orm: orm, @@ -150,8 +151,10 @@ func testORM(t *testing.T, language, orm string) { defer stopDB() td := testDriver{ - db: db, - dbName: app.dbName(), + db: db, + dbName: app.dbName(), + tableNames: tableNames, + columnNames: columnNames, } t.Run("FirstRun", func(t *testing.T) { @@ -231,25 +234,29 @@ func testORM(t *testing.T, language, orm string) { } func TestGORM(t *testing.T) { - testORM(t, "go", "gorm") + testORM(t, "go", "gorm", defaultTestTableNames, defaultTestColumnNames) } func TestHibernate(t *testing.T) { - testORM(t, "java", "hibernate") + testORM(t, "java", "hibernate", defaultTestTableNames, defaultTestColumnNames) } func TestSequelize(t *testing.T) { - testORM(t, "node", "sequelize") + testORM(t, "node", "sequelize", defaultTestTableNames, defaultTestColumnNames) } func TestSQLAlchemy(t *testing.T) { - testORM(t, "python", "sqlalchemy") + testORM(t, "python", "sqlalchemy", defaultTestTableNames, defaultTestColumnNames) +} + +func TestDjango(t *testing.T) { + testORM(t, "python", "django", djangoTestTableNames, djangoTestColumnNames) } func TestActiveRecord(t *testing.T) { - testORM(t, "ruby", "activerecord") + testORM(t, "ruby", "activerecord", defaultTestTableNames, defaultTestColumnNames) } func TestActiveRecord4(t *testing.T) { - testORM(t, "ruby", "ar4") + testORM(t, "ruby", "ar4", defaultTestTableNames, defaultTestColumnNames) } diff --git a/testing/test_driver.go b/testing/test_driver.go index c8f3b58255..94ccdd5317 100644 --- a/testing/test_driver.go +++ b/testing/test_driver.go @@ -11,12 +11,19 @@ import ( "github.com/cockroachdb/examples-orms/go/gorm/model" ) -const ( - customersTable = "customers" - ordersTable = "orders" - productsTable = "products" - orderProductsTable = "order_products" -) +type testTableNames struct { + customersTable string + ordersTable string + productsTable string + orderProductsTable string +} + +type testColumnNames struct { + customersColumns []string + ordersColumns []string + productsColumns []string + ordersProductsColumns []string +} // These need to be variables so that their address can be taken. var ( @@ -25,6 +32,34 @@ var ( productName1 = "Ice Cream" productPrice1 = "123.40" productPrice1Float = 123.40 + + defaultTestTableNames = testTableNames{ + customersTable: "customers", + ordersTable: "orders", + productsTable: "products", + orderProductsTable: "order_products", + } + + defaultTestColumnNames = testColumnNames{ + customersColumns: []string{"id", "name"}, + ordersColumns: []string{"customer_id", "id", "subtotal"}, + productsColumns: []string{"id", "name", "price"}, + ordersProductsColumns: []string{"order_id", "product_id"}, + } + + djangoTestTableNames = testTableNames{ + customersTable: "cockroach_example_customers", + ordersTable: "cockroach_example_orders", + productsTable: "cockroach_example_products", + orderProductsTable: "cockroach_example_orders_product", + } + + djangoTestColumnNames = testColumnNames{ + customersColumns: []string{"id", "name"}, + ordersColumns: []string{"customer_id", "id", "subtotal"}, + productsColumns: []string{"id", "name", "price"}, + ordersProductsColumns: []string{"id", "orders_id", "products_id"}, + } ) // parallelTestGroup maps a set of names to test functions, and will run each @@ -46,14 +81,18 @@ type testDriver struct { db *sql.DB dbName string api apiHandler + // Holds the expected table names for this test. + tableNames testTableNames + // Holds the expected columns for this test. + columnNames testColumnNames } func (td testDriver) TestGeneratedTables(t *testing.T) { exp := []string{ - customersTable, - orderProductsTable, - ordersTable, - productsTable, + td.tableNames.customersTable, + td.tableNames.orderProductsTable, + td.tableNames.ordersTable, + td.tableNames.productsTable, } actual := make(map[string]interface{}, len(exp)) @@ -77,20 +116,20 @@ ORDER BY 1`, td.dbName) } func (td testDriver) TestGeneratedCustomersTableColumns(t *testing.T) { - exp := []string{"id", "name"} - td.testGeneratedColumnsForTable(t, customersTable, exp) + td.testGeneratedColumnsForTable(t, td.tableNames.customersTable, + td.columnNames.customersColumns) } func (td testDriver) TestGeneratedOrdersTableColumns(t *testing.T) { - exp := []string{"customer_id", "id", "subtotal"} - td.testGeneratedColumnsForTable(t, ordersTable, exp) + td.testGeneratedColumnsForTable(t, td.tableNames.ordersTable, + td.columnNames.ordersColumns) } func (td testDriver) TestGeneratedProductsTableColumns(t *testing.T) { - exp := []string{"id", "name", "price"} - td.testGeneratedColumnsForTable(t, productsTable, exp) + td.testGeneratedColumnsForTable(t, td.tableNames.productsTable, + td.columnNames.productsColumns) } func (td testDriver) TestGeneratedOrderProductsTableColumns(t *testing.T) { - exp := []string{"order_id", "product_id"} - td.testGeneratedColumnsForTable(t, orderProductsTable, exp) + td.testGeneratedColumnsForTable(t, td.tableNames.orderProductsTable, + td.columnNames.ordersProductsColumns) } func (td testDriver) testGeneratedColumnsForTable(t *testing.T, table string, columns []string) { td.queryAndAssert(t, columns, ` @@ -104,16 +143,16 @@ ORDER BY 1`, td.dbName, table) } func (td testDriver) TestCustomersEmpty(t *testing.T) { - td.testTableEmpty(t, productsTable) + td.testTableEmpty(t, td.tableNames.productsTable) } func (td testDriver) TestOrdersTableEmpty(t *testing.T) { - td.testTableEmpty(t, customersTable) + td.testTableEmpty(t, td.tableNames.customersTable) } func (td testDriver) TestProductsTableEmpty(t *testing.T) { - td.testTableEmpty(t, ordersTable) + td.testTableEmpty(t, td.tableNames.ordersTable) } func (td testDriver) TestOrderProductsTableEmpty(t *testing.T) { - td.testTableEmpty(t, orderProductsTable) + td.testTableEmpty(t, td.tableNames.orderProductsTable) } func (td testDriver) testTableEmpty(t *testing.T, table string) { td.queryAndAssert(t, []string{"0"}, fmt.Sprintf(`SELECT COUNT(*) FROM %s`, table)) @@ -157,18 +196,20 @@ func (td testDriver) TestCreateCustomer(t *testing.T) { if err := td.api.createCustomer(customerName1); err != nil { t.Fatalf("error creating customer: %v", err) } - td.queryAndAssert(t, []string{customerName1}, fmt.Sprintf(`SELECT name FROM %s`, customersTable)) + td.queryAndAssert(t, []string{customerName1}, + fmt.Sprintf(`SELECT name FROM %s`, td.tableNames.customersTable)) } func (td testDriver) TestCreateProduct(t *testing.T) { if err := td.api.createProduct(productName1, productPrice1Float); err != nil { t.Fatalf("error creating product: %v", err) } - td.queryAndAssert(t, []string{row(productName1, productPrice1)}, fmt.Sprintf(`SELECT name, price FROM %s`, productsTable)) + td.queryAndAssert(t, []string{row(productName1, productPrice1)}, + fmt.Sprintf(`SELECT name, price FROM %s`, td.tableNames.productsTable)) } func (td testDriver) TestCreateOrder(t *testing.T) { // Get the single customer ID. - customerIDs, err := td.queryIDs(t, customersTable) + customerIDs, err := td.queryIDs(t, td.tableNames.customersTable) if err != nil { t.Fatal(err) } @@ -178,7 +219,7 @@ func (td testDriver) TestCreateOrder(t *testing.T) { customerID := customerIDs[0] // Get the single product. - productIDs, err := td.queryIDs(t, productsTable) + productIDs, err := td.queryIDs(t, td.tableNames.productsTable) if err != nil { t.Fatal(err) } @@ -190,7 +231,8 @@ func (td testDriver) TestCreateOrder(t *testing.T) { if err := td.api.createOrder(customerID, productID, productPrice1Float); err != nil { t.Fatalf("error creating order: %v", err) } - td.queryAndAssert(t, []string{row(productPrice1)}, fmt.Sprintf(`SELECT subtotal FROM %s`, ordersTable)) + td.queryAndAssert(t, []string{row(productPrice1)}, + fmt.Sprintf(`SELECT subtotal FROM %s`, td.tableNames.ordersTable)) } func (td testDriver) TestRetrieveCustomerAfterCreation(t *testing.T) { @@ -264,7 +306,9 @@ func (td testDriver) query(t *testing.T, query string, args ...interface{}) []st return found } -func (td testDriver) queryAndAssert(t *testing.T, expected []string, query string, args ...interface{}) { +func (td testDriver) queryAndAssert( + t *testing.T, expected []string, query string, args ...interface{}, +) { found := td.query(t, query, args...) if !reflect.DeepEqual(expected, found) {